diff --git a/src/test/java/us/eunoians/mcrpg/event/quest/chain/QuestChainAbandonEventTest.java b/src/test/java/us/eunoians/mcrpg/event/quest/chain/QuestChainAbandonEventTest.java new file mode 100644 index 000000000..0fec35580 --- /dev/null +++ b/src/test/java/us/eunoians/mcrpg/event/quest/chain/QuestChainAbandonEventTest.java @@ -0,0 +1,66 @@ +package us.eunoians.mcrpg.event.quest.chain; + +import org.bukkit.NamespacedKey; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; +import us.eunoians.mcrpg.McRPGBaseTest; +import us.eunoians.mcrpg.quest.chain.QuestChainDefinition; +import us.eunoians.mcrpg.quest.chain.QuestChainStep; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +@DisplayName("QuestChainAbandonEvent") +class QuestChainAbandonEventTest extends McRPGBaseTest { + + private static QuestChainDefinition buildDefinition() { + NamespacedKey chainKey = new NamespacedKey("mcrpg", "test_chain"); + NamespacedKey sourceKey = new NamespacedKey("mcrpg", "manual"); + NamespacedKey triggerKey = new NamespacedKey("mcrpg", "manual"); + NamespacedKey questKey = new NamespacedKey("mcrpg", "example_quest"); + List steps = List.of(QuestChainStep.simple(questKey)); + return new QuestChainDefinition.Builder(chainKey, sourceKey, triggerKey, steps).build(); + } + + @Test + @DisplayName("Given an online player, when getters are called, then they return the values passed to the constructor") + void event_getters_returnConstructorValues_whenPlayerOnline() { + QuestChainDefinition definition = buildDefinition(); + PlayerMock player = server.addPlayer(); + UUID playerUUID = player.getUniqueId(); + + QuestChainAbandonEvent event = new QuestChainAbandonEvent(definition, player, playerUUID); + + assertSame(definition, event.getChainDefinition()); + assertSame(player, event.getPlayer()); + assertEquals(playerUUID, event.getPlayerUUID()); + } + + @Test + @DisplayName("Given a null player (offline), when getPlayer is called, then it returns null") + void event_getPlayer_returnsNull_whenPlayerOffline() { + QuestChainDefinition definition = buildDefinition(); + UUID playerUUID = UUID.randomUUID(); + + QuestChainAbandonEvent event = new QuestChainAbandonEvent(definition, null, playerUUID); + + assertNull(event.getPlayer()); + assertEquals(playerUUID, event.getPlayerUUID()); + } + + @Test + @DisplayName("Given a chain abandon event, when getHandlers is called, then it matches the static handler list") + void event_getHandlers_matchesStaticHandlerList() { + QuestChainDefinition definition = buildDefinition(); + UUID playerUUID = UUID.randomUUID(); + + QuestChainAbandonEvent event = new QuestChainAbandonEvent(definition, null, playerUUID); + + assertEquals(QuestChainAbandonEvent.getHandlerList(), event.getHandlers()); + } +} diff --git a/src/test/java/us/eunoians/mcrpg/event/quest/chain/QuestChainExpireEventTest.java b/src/test/java/us/eunoians/mcrpg/event/quest/chain/QuestChainExpireEventTest.java new file mode 100644 index 000000000..115bb1db9 --- /dev/null +++ b/src/test/java/us/eunoians/mcrpg/event/quest/chain/QuestChainExpireEventTest.java @@ -0,0 +1,94 @@ +package us.eunoians.mcrpg.event.quest.chain; + +import org.bukkit.NamespacedKey; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; +import us.eunoians.mcrpg.McRPGBaseTest; +import us.eunoians.mcrpg.quest.chain.QuestChainDefinition; +import us.eunoians.mcrpg.quest.chain.QuestChainStep; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DisplayName("QuestChainExpireEvent") +class QuestChainExpireEventTest extends McRPGBaseTest { + + private static QuestChainDefinition buildDefinition() { + NamespacedKey chainKey = new NamespacedKey("mcrpg", "test_chain"); + NamespacedKey sourceKey = new NamespacedKey("mcrpg", "manual"); + NamespacedKey triggerKey = new NamespacedKey("mcrpg", "manual"); + NamespacedKey questKey = new NamespacedKey("mcrpg", "example_quest"); + List steps = List.of(QuestChainStep.simple(questKey)); + return new QuestChainDefinition.Builder(chainKey, sourceKey, triggerKey, steps).build(); + } + + @Test + @DisplayName("Given an online player, when getters are called, then they return the values passed to the constructor") + void event_getters_returnConstructorValues_whenPlayerOnline() { + QuestChainDefinition definition = buildDefinition(); + PlayerMock player = server.addPlayer(); + UUID playerUUID = player.getUniqueId(); + + QuestChainExpireEvent event = new QuestChainExpireEvent(definition, playerUUID, player); + + assertSame(definition, event.getChainDefinition()); + assertTrue(event.getPlayer().isPresent()); + assertSame(player, event.getPlayer().get()); + assertEquals(playerUUID, event.getPlayerUUID()); + } + + @Test + @DisplayName("Given a null player (offline), when getPlayer is called, then it returns empty Optional") + void event_getPlayer_returnsEmpty_whenPlayerOffline() { + QuestChainDefinition definition = buildDefinition(); + UUID playerUUID = UUID.randomUUID(); + + QuestChainExpireEvent event = new QuestChainExpireEvent(definition, playerUUID, null); + + assertTrue(event.getPlayer().isEmpty()); + assertEquals(playerUUID, event.getPlayerUUID()); + } + + @Test + @DisplayName("Given a chain expire event, when getHandlers is called, then it matches the static handler list") + void event_getHandlers_matchesStaticHandlerList() { + QuestChainDefinition definition = buildDefinition(); + UUID playerUUID = UUID.randomUUID(); + + QuestChainExpireEvent event = new QuestChainExpireEvent(definition, playerUUID, null); + + assertEquals(QuestChainExpireEvent.getHandlerList(), event.getHandlers()); + } + + @Test + @DisplayName("Given a chain expire event, then it is not cancelled by default") + void event_isNotCancelled_byDefault() { + QuestChainDefinition definition = buildDefinition(); + UUID playerUUID = UUID.randomUUID(); + + QuestChainExpireEvent event = new QuestChainExpireEvent(definition, playerUUID, null); + + assertFalse(event.isCancelled()); + } + + @Test + @DisplayName("Given a chain expire event, when setCancelled is called, then isCancelled reflects the change") + void event_setCancelled_updatesState() { + QuestChainDefinition definition = buildDefinition(); + UUID playerUUID = UUID.randomUUID(); + + QuestChainExpireEvent event = new QuestChainExpireEvent(definition, playerUUID, null); + + event.setCancelled(true); + assertTrue(event.isCancelled()); + + event.setCancelled(false); + assertFalse(event.isCancelled()); + } +} diff --git a/src/test/java/us/eunoians/mcrpg/event/quest/chain/QuestChainFailEventTest.java b/src/test/java/us/eunoians/mcrpg/event/quest/chain/QuestChainFailEventTest.java new file mode 100644 index 000000000..b29b9be32 --- /dev/null +++ b/src/test/java/us/eunoians/mcrpg/event/quest/chain/QuestChainFailEventTest.java @@ -0,0 +1,66 @@ +package us.eunoians.mcrpg.event.quest.chain; + +import org.bukkit.NamespacedKey; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; +import us.eunoians.mcrpg.McRPGBaseTest; +import us.eunoians.mcrpg.quest.chain.QuestChainDefinition; +import us.eunoians.mcrpg.quest.chain.QuestChainStep; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +@DisplayName("QuestChainFailEvent") +class QuestChainFailEventTest extends McRPGBaseTest { + + private static QuestChainDefinition buildDefinition() { + NamespacedKey chainKey = new NamespacedKey("mcrpg", "test_chain"); + NamespacedKey sourceKey = new NamespacedKey("mcrpg", "manual"); + NamespacedKey triggerKey = new NamespacedKey("mcrpg", "manual"); + NamespacedKey questKey = new NamespacedKey("mcrpg", "example_quest"); + List steps = List.of(QuestChainStep.simple(questKey)); + return new QuestChainDefinition.Builder(chainKey, sourceKey, triggerKey, steps).build(); + } + + @Test + @DisplayName("Given an online player, when getters are called, then they return the values passed to the constructor") + void event_getters_returnConstructorValues_whenPlayerOnline() { + QuestChainDefinition definition = buildDefinition(); + PlayerMock player = server.addPlayer(); + UUID playerUUID = player.getUniqueId(); + + QuestChainFailEvent event = new QuestChainFailEvent(definition, player, playerUUID); + + assertSame(definition, event.getChainDefinition()); + assertSame(player, event.getPlayer()); + assertEquals(playerUUID, event.getPlayerUUID()); + } + + @Test + @DisplayName("Given a null player (offline), when getPlayer is called, then it returns null") + void event_getPlayer_returnsNull_whenPlayerOffline() { + QuestChainDefinition definition = buildDefinition(); + UUID playerUUID = UUID.randomUUID(); + + QuestChainFailEvent event = new QuestChainFailEvent(definition, null, playerUUID); + + assertNull(event.getPlayer()); + assertEquals(playerUUID, event.getPlayerUUID()); + } + + @Test + @DisplayName("Given a chain fail event, when getHandlers is called, then it matches the static handler list") + void event_getHandlers_matchesStaticHandlerList() { + QuestChainDefinition definition = buildDefinition(); + UUID playerUUID = UUID.randomUUID(); + + QuestChainFailEvent event = new QuestChainFailEvent(definition, null, playerUUID); + + assertEquals(QuestChainFailEvent.getHandlerList(), event.getHandlers()); + } +} diff --git a/src/test/java/us/eunoians/mcrpg/event/quest/chain/QuestChainRestartEventTest.java b/src/test/java/us/eunoians/mcrpg/event/quest/chain/QuestChainRestartEventTest.java new file mode 100644 index 000000000..5eea9a4a9 --- /dev/null +++ b/src/test/java/us/eunoians/mcrpg/event/quest/chain/QuestChainRestartEventTest.java @@ -0,0 +1,84 @@ +package us.eunoians.mcrpg.event.quest.chain; + +import org.bukkit.NamespacedKey; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockbukkit.mockbukkit.entity.PlayerMock; +import us.eunoians.mcrpg.McRPGBaseTest; +import us.eunoians.mcrpg.quest.chain.QuestChainDefinition; +import us.eunoians.mcrpg.quest.chain.QuestChainStep; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +@DisplayName("QuestChainRestartEvent") +class QuestChainRestartEventTest extends McRPGBaseTest { + + private static QuestChainDefinition buildDefinition() { + NamespacedKey chainKey = new NamespacedKey("mcrpg", "test_chain"); + NamespacedKey sourceKey = new NamespacedKey("mcrpg", "manual"); + NamespacedKey triggerKey = new NamespacedKey("mcrpg", "manual"); + NamespacedKey questKey = new NamespacedKey("mcrpg", "example_quest"); + List steps = List.of(QuestChainStep.simple(questKey)); + return new QuestChainDefinition.Builder(chainKey, sourceKey, triggerKey, steps).build(); + } + + @Test + @DisplayName("Given an online player, when getters are called, then they return the values passed to the constructor") + void event_getters_returnConstructorValues_whenPlayerOnline() { + QuestChainDefinition definition = buildDefinition(); + PlayerMock player = server.addPlayer(); + UUID playerUUID = player.getUniqueId(); + QuestChainRestartEvent.RestartReason reason = QuestChainRestartEvent.RestartReason.REPEAT_MODE; + + QuestChainRestartEvent event = new QuestChainRestartEvent(definition, player, playerUUID, reason); + + assertSame(definition, event.getChainDefinition()); + assertSame(player, event.getPlayer()); + assertEquals(playerUUID, event.getPlayerUUID()); + assertEquals(reason, event.getReason()); + } + + @Test + @DisplayName("Given a null player (offline), when getPlayer is called, then it returns null") + void event_getPlayer_returnsNull_whenPlayerOffline() { + QuestChainDefinition definition = buildDefinition(); + UUID playerUUID = UUID.randomUUID(); + + QuestChainRestartEvent event = new QuestChainRestartEvent(definition, null, playerUUID, + QuestChainRestartEvent.RestartReason.QUEST_EXPIRE_RESTART_CHAIN); + + assertNull(event.getPlayer()); + assertEquals(playerUUID, event.getPlayerUUID()); + } + + @ParameterizedTest + @EnumSource(QuestChainRestartEvent.RestartReason.class) + @DisplayName("Given each RestartReason variant, when getReason is called, then it returns the correct reason") + void event_getReason_returnsCorrectVariant(QuestChainRestartEvent.RestartReason reason) { + QuestChainDefinition definition = buildDefinition(); + UUID playerUUID = UUID.randomUUID(); + + QuestChainRestartEvent event = new QuestChainRestartEvent(definition, null, playerUUID, reason); + + assertEquals(reason, event.getReason()); + } + + @Test + @DisplayName("Given a chain restart event, when getHandlers is called, then it matches the static handler list") + void event_getHandlers_matchesStaticHandlerList() { + QuestChainDefinition definition = buildDefinition(); + UUID playerUUID = UUID.randomUUID(); + + QuestChainRestartEvent event = new QuestChainRestartEvent(definition, null, playerUUID, + QuestChainRestartEvent.RestartReason.REPEAT_MODE); + + assertEquals(QuestChainRestartEvent.getHandlerList(), event.getHandlers()); + } +} diff --git a/src/test/java/us/eunoians/mcrpg/event/quest/chain/QuestChainStepRetryEventTest.java b/src/test/java/us/eunoians/mcrpg/event/quest/chain/QuestChainStepRetryEventTest.java new file mode 100644 index 000000000..8ce9f9aac --- /dev/null +++ b/src/test/java/us/eunoians/mcrpg/event/quest/chain/QuestChainStepRetryEventTest.java @@ -0,0 +1,96 @@ +package us.eunoians.mcrpg.event.quest.chain; + +import org.bukkit.NamespacedKey; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; +import us.eunoians.mcrpg.McRPGBaseTest; +import us.eunoians.mcrpg.quest.chain.QuestChainDefinition; +import us.eunoians.mcrpg.quest.chain.QuestChainStep; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +@DisplayName("QuestChainStepRetryEvent") +class QuestChainStepRetryEventTest extends McRPGBaseTest { + + private static QuestChainDefinition buildDefinition() { + NamespacedKey chainKey = new NamespacedKey("mcrpg", "test_chain"); + NamespacedKey sourceKey = new NamespacedKey("mcrpg", "manual"); + NamespacedKey triggerKey = new NamespacedKey("mcrpg", "manual"); + NamespacedKey questKey = new NamespacedKey("mcrpg", "example_quest"); + List steps = List.of(QuestChainStep.simple(questKey)); + return new QuestChainDefinition.Builder(chainKey, sourceKey, triggerKey, steps).build(); + } + + @Test + @DisplayName("Given an online player, when getters are called, then they return the values passed to the constructor") + void event_getters_returnConstructorValues_whenPlayerOnline() { + QuestChainDefinition definition = buildDefinition(); + PlayerMock player = server.addPlayer(); + UUID playerUUID = player.getUniqueId(); + QuestChainStep step = definition.getSteps().get(0); + + QuestChainStepRetryEvent event = new QuestChainStepRetryEvent(definition, player, playerUUID, step, 2, 5); + + assertSame(definition, event.getChainDefinition()); + assertSame(player, event.getPlayer()); + assertEquals(playerUUID, event.getPlayerUUID()); + assertSame(step, event.getStep()); + assertEquals(2, event.getRetryNumber()); + assertEquals(5, event.getMaxRetries()); + } + + @Test + @DisplayName("Given a null player (offline), when getPlayer is called, then it returns null") + void event_getPlayer_returnsNull_whenPlayerOffline() { + QuestChainDefinition definition = buildDefinition(); + UUID playerUUID = UUID.randomUUID(); + QuestChainStep step = definition.getSteps().get(0); + + QuestChainStepRetryEvent event = new QuestChainStepRetryEvent(definition, null, playerUUID, step, 1, 3); + + assertNull(event.getPlayer()); + assertEquals(playerUUID, event.getPlayerUUID()); + } + + @Test + @DisplayName("Given unlimited retries (-1), when getMaxRetries is called, then it returns -1") + void event_getMaxRetries_returnsNegativeOne_whenUnlimited() { + QuestChainDefinition definition = buildDefinition(); + UUID playerUUID = UUID.randomUUID(); + QuestChainStep step = definition.getSteps().get(0); + + QuestChainStepRetryEvent event = new QuestChainStepRetryEvent(definition, null, playerUUID, step, 1, -1); + + assertEquals(-1, event.getMaxRetries()); + } + + @Test + @DisplayName("Given a first retry, when getRetryNumber is called, then it returns 1") + void event_getRetryNumber_returnsOne_whenFirstRetry() { + QuestChainDefinition definition = buildDefinition(); + UUID playerUUID = UUID.randomUUID(); + QuestChainStep step = definition.getSteps().get(0); + + QuestChainStepRetryEvent event = new QuestChainStepRetryEvent(definition, null, playerUUID, step, 1, 3); + + assertEquals(1, event.getRetryNumber()); + } + + @Test + @DisplayName("Given a step retry event, when getHandlers is called, then it matches the static handler list") + void event_getHandlers_matchesStaticHandlerList() { + QuestChainDefinition definition = buildDefinition(); + UUID playerUUID = UUID.randomUUID(); + QuestChainStep step = definition.getSteps().get(0); + + QuestChainStepRetryEvent event = new QuestChainStepRetryEvent(definition, null, playerUUID, step, 1, 3); + + assertEquals(QuestChainStepRetryEvent.getHandlerList(), event.getHandlers()); + } +} diff --git a/src/test/java/us/eunoians/mcrpg/listener/combat/CombatDamageResolverTest.java b/src/test/java/us/eunoians/mcrpg/listener/combat/CombatDamageResolverTest.java new file mode 100644 index 000000000..02cdafd09 --- /dev/null +++ b/src/test/java/us/eunoians/mcrpg/listener/combat/CombatDamageResolverTest.java @@ -0,0 +1,212 @@ +package us.eunoians.mcrpg.listener.combat; + +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.entity.Projectile; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import us.eunoians.mcrpg.McRPGBaseTest; + +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@DisplayName("CombatDamageResolver") +class CombatDamageResolverTest extends McRPGBaseTest { + + private CombatDamageResolver resolver; + + @BeforeEach + void setUp() { + resolver = new CombatDamageResolver(); + } + + @Nested + @DisplayName("resolve()") + class ResolveTests { + + @Test + @DisplayName("Given a direct melee hit between two LivingEntities, then returns Combatants with correct source and target") + void resolve_returnsCombatants_whenDirectMeleeHit() { + LivingEntity attacker = mock(LivingEntity.class); + LivingEntity victim = mock(LivingEntity.class); + when(attacker.getUniqueId()).thenReturn(UUID.randomUUID()); + when(victim.getUniqueId()).thenReturn(UUID.randomUUID()); + + EntityDamageByEntityEvent event = mock(EntityDamageByEntityEvent.class); + when(event.getDamager()).thenReturn(attacker); + when(event.getEntity()).thenReturn(victim); + + Optional result = resolver.resolve(event); + + assertTrue(result.isPresent()); + assertSame(attacker, result.get().source()); + assertSame(victim, result.get().target()); + } + + @Test + @DisplayName("Given a projectile with an Entity shooter, then unwraps to the shooter as source") + void resolve_unwrapsProjectileToShooter_whenShooterIsEntity() { + Player shooter = mock(Player.class); + UUID shooterUUID = UUID.randomUUID(); + when(shooter.getUniqueId()).thenReturn(shooterUUID); + + // Projectile that also implements Entity (mock both interfaces via the Projectile type) + Projectile projectile = mock(Projectile.class); + when(projectile.getShooter()).thenReturn(shooter); + + LivingEntity target = mock(LivingEntity.class); + when(target.getUniqueId()).thenReturn(UUID.randomUUID()); + + EntityDamageByEntityEvent event = mock(EntityDamageByEntityEvent.class); + when(event.getDamager()).thenReturn(projectile); + when(event.getEntity()).thenReturn(target); + + Optional result = resolver.resolve(event); + + assertTrue(result.isPresent()); + assertSame(shooter, result.get().source()); + assertSame(target, result.get().target()); + } + + @Test + @DisplayName("Given a projectile whose shooter is not an Entity, then returns empty") + void resolve_returnsEmpty_whenProjectileShooterIsNotEntity() { + Projectile projectile = mock(Projectile.class); + when(projectile.getShooter()).thenReturn(mock(org.bukkit.projectiles.ProjectileSource.class)); + + LivingEntity target = mock(LivingEntity.class); + + EntityDamageByEntityEvent event = mock(EntityDamageByEntityEvent.class); + when(event.getDamager()).thenReturn(projectile); + when(event.getEntity()).thenReturn(target); + + Optional result = resolver.resolve(event); + + assertTrue(result.isEmpty()); + } + + @Test + @DisplayName("Given a projectile with a null shooter, then returns empty") + void resolve_returnsEmpty_whenProjectileShooterIsNull() { + Projectile projectile = mock(Projectile.class); + when(projectile.getShooter()).thenReturn(null); + + LivingEntity target = mock(LivingEntity.class); + + EntityDamageByEntityEvent event = mock(EntityDamageByEntityEvent.class); + when(event.getDamager()).thenReturn(projectile); + when(event.getEntity()).thenReturn(target); + + Optional result = resolver.resolve(event); + + assertTrue(result.isEmpty()); + } + + @Test + @DisplayName("Given a non-LivingEntity damager, then returns empty") + void resolve_returnsEmpty_whenDamagerIsNotLivingEntity() { + Entity nonLivingDamager = mock(Entity.class); + + LivingEntity target = mock(LivingEntity.class); + + EntityDamageByEntityEvent event = mock(EntityDamageByEntityEvent.class); + when(event.getDamager()).thenReturn(nonLivingDamager); + when(event.getEntity()).thenReturn(target); + + Optional result = resolver.resolve(event); + + assertTrue(result.isEmpty()); + } + + @Test + @DisplayName("Given a non-LivingEntity target, then returns empty") + void resolve_returnsEmpty_whenTargetIsNotLivingEntity() { + LivingEntity attacker = mock(LivingEntity.class); + when(attacker.getUniqueId()).thenReturn(UUID.randomUUID()); + + Entity nonLivingTarget = mock(Entity.class); + + EntityDamageByEntityEvent event = mock(EntityDamageByEntityEvent.class); + when(event.getDamager()).thenReturn(attacker); + when(event.getEntity()).thenReturn(nonLivingTarget); + + Optional result = resolver.resolve(event); + + assertTrue(result.isEmpty()); + } + + @Test + @DisplayName("Given self-damage (same UUID), then returns empty") + void resolve_returnsEmpty_whenSelfDamage() { + UUID sharedUUID = UUID.randomUUID(); + + LivingEntity entity = mock(LivingEntity.class); + when(entity.getUniqueId()).thenReturn(sharedUUID); + + EntityDamageByEntityEvent event = mock(EntityDamageByEntityEvent.class); + when(event.getDamager()).thenReturn(entity); + when(event.getEntity()).thenReturn(entity); + + Optional result = resolver.resolve(event); + + assertTrue(result.isEmpty()); + } + } + + @Nested + @DisplayName("Combatants record") + class CombatantsTests { + + @Test + @DisplayName("sourceUUID returns the source entity's UUID") + void sourceUUID_returnsSourceEntityUUID() { + UUID sourceUUID = UUID.randomUUID(); + LivingEntity source = mock(LivingEntity.class); + when(source.getUniqueId()).thenReturn(sourceUUID); + LivingEntity target = mock(LivingEntity.class); + when(target.getUniqueId()).thenReturn(UUID.randomUUID()); + + CombatDamageResolver.Combatants combatants = new CombatDamageResolver.Combatants(source, target); + + assertEquals(sourceUUID, combatants.sourceUUID()); + } + + @Test + @DisplayName("targetUUID returns the target entity's UUID") + void targetUUID_returnsTargetEntityUUID() { + UUID targetUUID = UUID.randomUUID(); + LivingEntity source = mock(LivingEntity.class); + when(source.getUniqueId()).thenReturn(UUID.randomUUID()); + LivingEntity target = mock(LivingEntity.class); + when(target.getUniqueId()).thenReturn(targetUUID); + + CombatDamageResolver.Combatants combatants = new CombatDamageResolver.Combatants(source, target); + + assertEquals(targetUUID, combatants.targetUUID()); + } + + @Test + @DisplayName("source and target accessors return the entities passed to the constructor") + void accessors_returnConstructorEntities() { + LivingEntity source = mock(LivingEntity.class); + when(source.getUniqueId()).thenReturn(UUID.randomUUID()); + LivingEntity target = mock(LivingEntity.class); + when(target.getUniqueId()).thenReturn(UUID.randomUUID()); + + CombatDamageResolver.Combatants combatants = new CombatDamageResolver.Combatants(source, target); + + assertSame(source, combatants.source()); + assertSame(target, combatants.target()); + } + } +}