diff --git a/build.gradle.kts b/build.gradle.kts index d548155..fbb4853 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -96,6 +96,7 @@ dependencies { // Custom items val itemsAdderVersion = "4.0.10" compileOnly("dev.lone:api-itemsadder:$itemsAdderVersion") + testImplementation("dev.lone:api-itemsadder:$itemsAdderVersion") val nexoVersion = "1.16.0" compileOnly("com.nexomc:nexo:$nexoVersion") { exclude(group = "net.byteflux") @@ -130,11 +131,15 @@ dependencies { val cmiVersion = "9.7.14.3" compileOnly("com.github.Zrips:CMI-API:$cmiVersion") + testImplementation("com.github.Zrips:CMI-API:$cmiVersion") val citizensVersion = "2.0.39-SNAPSHOT" compileOnly("net.citizensnpcs:citizens-main:$citizensVersion") { exclude(group = "*", module = "*") } + testImplementation("net.citizensnpcs:citizens-main:$citizensVersion") { + exclude(group = "*", module = "*") + } } diff --git a/src/test/java/com/diamonddagger590/mccore/external/citizens/CoreCitizensHookTest.java b/src/test/java/com/diamonddagger590/mccore/external/citizens/CoreCitizensHookTest.java new file mode 100644 index 0000000..ceec4c9 --- /dev/null +++ b/src/test/java/com/diamonddagger590/mccore/external/citizens/CoreCitizensHookTest.java @@ -0,0 +1,76 @@ +package com.diamonddagger590.mccore.external.citizens; + +import com.diamonddagger590.mccore.CorePlugin; +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.api.npc.NPCRegistry; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CoreCitizensHookTest { + + @Mock + private CorePlugin plugin; + + private CoreCitizensHook hook; + + @BeforeEach + void setUp() { + hook = new CoreCitizensHook(plugin); + } + + @Test + @DisplayName("Returns true when entity is an NPC according to Citizens") + void isEntityNpc_returnsTrue_whenCitizensReportsNpc() { + Entity entity = mock(Entity.class); + + try (MockedStatic mocked = mockStatic(CitizensAPI.class)) { + NPCRegistry npcRegistry = mock(NPCRegistry.class); + mocked.when(CitizensAPI::getNPCRegistry).thenReturn(npcRegistry); + when(npcRegistry.isNPC(entity)).thenReturn(true); + + assertTrue(hook.isEntityNpc(entity)); + } + } + + @Test + @DisplayName("Returns false when entity is not an NPC according to Citizens") + void isEntityNpc_returnsFalse_whenCitizensReportsNotNpc() { + Entity entity = mock(Entity.class); + + try (MockedStatic mocked = mockStatic(CitizensAPI.class)) { + NPCRegistry npcRegistry = mock(NPCRegistry.class); + mocked.when(CitizensAPI::getNPCRegistry).thenReturn(npcRegistry); + when(npcRegistry.isNPC(entity)).thenReturn(false); + + assertFalse(hook.isEntityNpc(entity)); + } + } + + @Test + @DisplayName("Returns true when entity is a Player and Citizens reports NPC") + void isEntityNpc_returnsTrue_whenPlayerIsNpc() { + Player player = mock(Player.class); + + try (MockedStatic mocked = mockStatic(CitizensAPI.class)) { + NPCRegistry npcRegistry = mock(NPCRegistry.class); + mocked.when(CitizensAPI::getNPCRegistry).thenReturn(npcRegistry); + when(npcRegistry.isNPC(player)).thenReturn(true); + + assertTrue(hook.isEntityNpc(player)); + } + } +} diff --git a/src/test/java/com/diamonddagger590/mccore/external/cmi/CoreCMIHookTest.java b/src/test/java/com/diamonddagger590/mccore/external/cmi/CoreCMIHookTest.java new file mode 100644 index 0000000..f400001 --- /dev/null +++ b/src/test/java/com/diamonddagger590/mccore/external/cmi/CoreCMIHookTest.java @@ -0,0 +1,156 @@ +package com.diamonddagger590.mccore.external.cmi; + +import com.Zrips.CMI.CMI; +import com.Zrips.CMI.Containers.CMIUser; +import com.Zrips.CMI.PlayerManager; +import com.Zrips.CMI.utils.Util; +import com.diamonddagger590.mccore.CorePlugin; +import com.diamonddagger590.mccore.player.CorePlayer; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +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 org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CoreCMIHookTest { + + @Mock + private CorePlugin plugin; + + private CoreCMIHook hook; + + @BeforeEach + void setUp() { + hook = new CoreCMIHook(plugin); + } + + @Nested + @DisplayName("isAfk") + class IsAfkTests { + + @Mock + private CorePlayer corePlayer; + + @Test + @DisplayName("Returns true when player is online and CMI reports AFK") + void isAfk_returnsTrue_whenPlayerOnlineAndAfk() { + Player player = mock(Player.class); + when(corePlayer.getAsBukkitPlayer()).thenReturn(Optional.of(player)); + + try (MockedStatic mocked = mockStatic(CMI.class)) { + CMI cmi = mock(CMI.class); + mocked.when(CMI::getInstance).thenReturn(cmi); + PlayerManager playerManager = mock(PlayerManager.class); + when(cmi.getPlayerManager()).thenReturn(playerManager); + CMIUser cmiUser = mock(CMIUser.class); + when(playerManager.getUser(player)).thenReturn(cmiUser); + when(cmiUser.isAfk()).thenReturn(true); + + assertTrue(hook.isAfk(corePlayer)); + } + } + + @Test + @DisplayName("Returns false when player is online and CMI reports not AFK") + void isAfk_returnsFalse_whenPlayerOnlineAndNotAfk() { + Player player = mock(Player.class); + when(corePlayer.getAsBukkitPlayer()).thenReturn(Optional.of(player)); + + try (MockedStatic mocked = mockStatic(CMI.class)) { + CMI cmi = mock(CMI.class); + mocked.when(CMI::getInstance).thenReturn(cmi); + PlayerManager playerManager = mock(PlayerManager.class); + when(cmi.getPlayerManager()).thenReturn(playerManager); + CMIUser cmiUser = mock(CMIUser.class); + when(playerManager.getUser(player)).thenReturn(cmiUser); + when(cmiUser.isAfk()).thenReturn(false); + + assertFalse(hook.isAfk(corePlayer)); + } + } + + @Test + @DisplayName("Returns false when CMIUser is null") + void isAfk_returnsFalse_whenCmiUserIsNull() { + Player player = mock(Player.class); + when(corePlayer.getAsBukkitPlayer()).thenReturn(Optional.of(player)); + + try (MockedStatic mocked = mockStatic(CMI.class)) { + CMI cmi = mock(CMI.class); + mocked.when(CMI::getInstance).thenReturn(cmi); + PlayerManager playerManager = mock(PlayerManager.class); + when(cmi.getPlayerManager()).thenReturn(playerManager); + when(playerManager.getUser(player)).thenReturn(null); + + assertFalse(hook.isAfk(corePlayer)); + } + } + + @Test + @DisplayName("Returns false when player is offline") + void isAfk_returnsFalse_whenPlayerOffline() { + when(corePlayer.getAsBukkitPlayer()).thenReturn(Optional.empty()); + + assertFalse(hook.isAfk(corePlayer)); + } + } + + @Nested + @DisplayName("isEntityNpc") + class IsEntityNpcTests { + + @Test + @DisplayName("Returns true when entity is a Player and CMI reports NPC") + void isEntityNpc_returnsTrue_whenPlayerIsNpc() { + Player player = mock(Player.class); + + try (MockedStatic mocked = mockStatic(CMI.class)) { + CMI cmi = mock(CMI.class); + mocked.when(CMI::getInstance).thenReturn(cmi); + Util utilManager = mock(Util.class); + when(cmi.getUtilManager()).thenReturn(utilManager); + when(utilManager.isNPC(player)).thenReturn(true); + + assertTrue(hook.isEntityNpc(player)); + } + } + + @Test + @DisplayName("Returns false when entity is a Player but CMI says not NPC") + void isEntityNpc_returnsFalse_whenPlayerIsNotNpc() { + Player player = mock(Player.class); + + try (MockedStatic mocked = mockStatic(CMI.class)) { + CMI cmi = mock(CMI.class); + mocked.when(CMI::getInstance).thenReturn(cmi); + Util utilManager = mock(Util.class); + when(cmi.getUtilManager()).thenReturn(utilManager); + when(utilManager.isNPC(player)).thenReturn(false); + + assertFalse(hook.isEntityNpc(player)); + } + } + + @Test + @DisplayName("Returns false when entity is not a Player") + void isEntityNpc_returnsFalse_whenEntityIsNotPlayer() { + Entity entity = mock(Entity.class); + + assertFalse(hook.isEntityNpc(entity)); + } + } +} diff --git a/src/test/java/com/diamonddagger590/mccore/external/itemsadder/CoreItemsAdderHookTest.java b/src/test/java/com/diamonddagger590/mccore/external/itemsadder/CoreItemsAdderHookTest.java new file mode 100644 index 0000000..1cb5dd1 --- /dev/null +++ b/src/test/java/com/diamonddagger590/mccore/external/itemsadder/CoreItemsAdderHookTest.java @@ -0,0 +1,837 @@ +package com.diamonddagger590.mccore.external.itemsadder; + +import com.diamonddagger590.mccore.CorePlugin; +import com.diamonddagger590.mccore.util.item.CustomBlockWrapper; +import com.diamonddagger590.mccore.util.item.CustomItemWrapper; +import dev.lone.itemsadder.api.CustomBlock; +import dev.lone.itemsadder.api.CustomStack; +import net.kyori.adventure.text.Component; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Particle; +import org.bukkit.SoundGroup; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Entity; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +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 org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CoreItemsAdderHookTest { + + @Mock + private CorePlugin plugin; + + private CoreItemsAdderHook hook; + + @BeforeEach + void setUp() { + hook = new CoreItemsAdderHook(plugin); + } + + @Nested + @DisplayName("item(String)") + class ItemTests { + + @Test + @DisplayName("Returns the ItemStack when CustomStack exists for the given item ID") + void item_returnsItemStack_whenCustomStackExists() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + ItemStack expectedItem = mock(ItemStack.class); + mocked.when(() -> CustomStack.getInstance("my_item")).thenReturn(customStack); + when(customStack.getItemStack()).thenReturn(expectedItem); + + Optional result = hook.item("my_item"); + + assertTrue(result.isPresent()); + assertEquals(expectedItem, result.get()); + } + } + + @Test + @DisplayName("Returns empty when CustomStack does not exist for the given item ID") + void item_returnsEmpty_whenCustomStackDoesNotExist() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + mocked.when(() -> CustomStack.getInstance("nonexistent")).thenReturn(null); + + Optional result = hook.item("nonexistent"); + + assertFalse(result.isPresent()); + } + } + } + + @Nested + @DisplayName("itemModels(ItemStack)") + class ItemModelsTests { + + @Test + @DisplayName("Returns model path set when CustomStack exists for the item") + void itemModels_returnsModelSet_whenCustomStackExists() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + ItemStack itemStack = mock(ItemStack.class); + mocked.when(() -> CustomStack.byItemStack(itemStack)).thenReturn(customStack); + when(customStack.getModelPath()).thenReturn("my_namespace:my_model"); + + Optional> result = hook.itemModels(itemStack); + + assertTrue(result.isPresent()); + assertEquals(Set.of("my_namespace:my_model"), result.get()); + } + } + + @Test + @DisplayName("Returns empty when CustomStack does not exist for the item") + void itemModels_returnsEmpty_whenNoCustomStack() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + ItemStack itemStack = mock(ItemStack.class); + mocked.when(() -> CustomStack.byItemStack(itemStack)).thenReturn(null); + + Optional> result = hook.itemModels(itemStack); + + assertFalse(result.isPresent()); + } + } + } + + @Nested + @DisplayName("isItem") + class IsItemTests { + + @Test + @DisplayName("isItem(String) returns true when item is in registry") + void isItem_string_returnsTrue_whenInRegistry() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + mocked.when(() -> CustomStack.isInRegistry("valid_item")).thenReturn(true); + + assertTrue(hook.isItem("valid_item")); + } + } + + @Test + @DisplayName("isItem(String) returns false when item is not in registry") + void isItem_string_returnsFalse_whenNotInRegistry() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + mocked.when(() -> CustomStack.isInRegistry("invalid_item")).thenReturn(false); + + assertFalse(hook.isItem("invalid_item")); + } + } + + @Test + @DisplayName("isItem(ItemStack) returns true when CustomStack exists") + void isItem_itemStack_returnsTrue_whenCustomStackExists() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + ItemStack itemStack = mock(ItemStack.class); + mocked.when(() -> CustomStack.byItemStack(itemStack)).thenReturn(mock(CustomStack.class)); + + assertTrue(hook.isItem(itemStack)); + } + } + + @Test + @DisplayName("isItem(ItemStack) returns false when CustomStack does not exist") + void isItem_itemStack_returnsFalse_whenNoCustomStack() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + ItemStack itemStack = mock(ItemStack.class); + mocked.when(() -> CustomStack.byItemStack(itemStack)).thenReturn(null); + + assertFalse(hook.isItem(itemStack)); + } + } + } + + @Nested + @DisplayName("isItemOfType") + class IsItemOfTypeTests { + + @Test + @DisplayName("Returns true when item matches the given type name") + void isItemOfType_returnsTrue_whenMatches() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + ItemStack itemStack = mock(ItemStack.class); + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.byItemStack(itemStack)).thenReturn(customStack); + when(customStack.getModelPath()).thenReturn("my_sword"); + + assertTrue(hook.isItemOfType(itemStack, "my_sword")); + } + } + + @Test + @DisplayName("Returns true when item matches type name case-insensitively") + void isItemOfType_returnsTrue_caseInsensitive() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + ItemStack itemStack = mock(ItemStack.class); + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.byItemStack(itemStack)).thenReturn(customStack); + when(customStack.getModelPath()).thenReturn("My_Sword"); + + assertTrue(hook.isItemOfType(itemStack, "my_sword")); + } + } + + @Test + @DisplayName("Returns false when no CustomStack exists for the item") + void isItemOfType_returnsFalse_whenNoCustomStack() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + ItemStack itemStack = mock(ItemStack.class); + mocked.when(() -> CustomStack.byItemStack(itemStack)).thenReturn(null); + + assertFalse(hook.isItemOfType(itemStack, "my_sword")); + } + } + + @Test + @DisplayName("Returns false when item type does not match") + void isItemOfType_returnsFalse_whenTypeMismatch() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + ItemStack itemStack = mock(ItemStack.class); + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.byItemStack(itemStack)).thenReturn(customStack); + when(customStack.getModelPath()).thenReturn("other_item"); + + assertFalse(hook.isItemOfType(itemStack, "my_sword")); + } + } + } + + @Nested + @DisplayName("Block operations") + class BlockTests { + + @Mock + private Block block; + + @Test + @DisplayName("isCustomBlock(Block) returns true when block is a custom block") + void isCustomBlock_returnsTrue_whenCustomBlockExists() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(mock(CustomBlock.class)); + + assertTrue(hook.isCustomBlock(block)); + } + } + + @Test + @DisplayName("isCustomBlock(Block) returns false when block is not a custom block") + void isCustomBlock_returnsFalse_whenNotCustomBlock() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(null); + + assertFalse(hook.isCustomBlock(block)); + } + } + + @Test + @DisplayName("isCustomBlock(String) always returns false for ItemsAdder") + void isCustomBlock_string_alwaysReturnsFalse() { + assertFalse(hook.isCustomBlock("any_block")); + } + + @Test + @DisplayName("isCustomBlockOfType returns true when block matches the type") + void isCustomBlockOfType_returnsTrue_whenMatches() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + CustomBlock customBlock = mock(CustomBlock.class); + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(customBlock); + when(customBlock.getModelPath()).thenReturn("my_block"); + + assertTrue(hook.isCustomBlockOfType(block, "my_block")); + } + } + + @Test + @DisplayName("isCustomBlockOfType returns true case-insensitively") + void isCustomBlockOfType_returnsTrue_caseInsensitive() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + CustomBlock customBlock = mock(CustomBlock.class); + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(customBlock); + when(customBlock.getModelPath()).thenReturn("My_Block"); + + assertTrue(hook.isCustomBlockOfType(block, "my_block")); + } + } + + @Test + @DisplayName("isCustomBlockOfType returns false when block is not custom") + void isCustomBlockOfType_returnsFalse_whenNotCustom() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(null); + + assertFalse(hook.isCustomBlockOfType(block, "my_block")); + } + } + + @Test + @DisplayName("isCustomBlockOfType returns false when type does not match") + void isCustomBlockOfType_returnsFalse_whenTypeMismatch() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + CustomBlock customBlock = mock(CustomBlock.class); + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(customBlock); + when(customBlock.getModelPath()).thenReturn("other_block"); + + assertFalse(hook.isCustomBlockOfType(block, "my_block")); + } + } + + @Test + @DisplayName("blockModels returns model set when custom block exists") + void blockModels_returnsModelSet_whenCustomBlockExists() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + CustomBlock customBlock = mock(CustomBlock.class); + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(customBlock); + when(customBlock.getModelPath()).thenReturn("ns:my_model"); + + Optional> result = hook.blockModels(block); + + assertTrue(result.isPresent()); + assertEquals(Set.of("ns:my_model"), result.get()); + } + } + + @Test + @DisplayName("blockModels returns empty when block is not custom") + void blockModels_returnsEmpty_whenNotCustom() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(null); + + assertFalse(hook.blockModels(block).isPresent()); + } + } + } + + @Nested + @DisplayName("placeCustomBlock") + class PlaceCustomBlockTests { + + @Test + @DisplayName("Throws when blockId is not a valid custom block") + void placeCustomBlock_throwsIllegalArgument_whenNotValidBlock() { + Location location = mock(Location.class); + + assertThrows(IllegalArgumentException.class, + () -> hook.placeCustomBlock(location, "invalid_block")); + } + + @Test + @DisplayName("Places block via CustomBlock.place when blockId passes validation") + void placeCustomBlock_placesBlock_whenBlockIsValid() { + Location location = mock(Location.class); + CoreItemsAdderHook spyHook = spy(hook); + doReturn(true).when(spyHook).isCustomBlock("ns:valid_block"); + + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + spyHook.placeCustomBlock(location, "ns:valid_block"); + + mocked.verify(() -> CustomBlock.place("ns:valid_block", location)); + } + } + } + + @Nested + @DisplayName("drops") + class DropsTests { + + @Mock + private Block block; + + @Test + @DisplayName("Returns custom block drops when block is a custom block") + void drops_returnsCustomDrops_whenCustomBlock() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + ItemStack tool = mock(ItemStack.class); + Entity entity = mock(Entity.class); + List expectedDrops = List.of(mock(ItemStack.class)); + + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(mock(CustomBlock.class)); + mocked.when(() -> CustomBlock.getLoot(block, tool, true)).thenReturn(expectedDrops); + + List result = hook.drops(block, tool, entity); + + assertEquals(expectedDrops, result); + } + } + + @SuppressWarnings("unchecked") + @Test + @DisplayName("Returns vanilla block drops when block is not custom") + void drops_returnsVanillaDrops_whenNotCustomBlock() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + ItemStack tool = mock(ItemStack.class); + Entity entity = mock(Entity.class); + ItemStack dropItem = mock(ItemStack.class); + Collection drops = List.of(dropItem); + + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(null); + when(block.getDrops(tool, entity)).thenReturn(drops); + + List result = hook.drops(block, tool, entity); + + assertEquals(1, result.size()); + assertTrue(result.contains(dropItem)); + } + } + + @SuppressWarnings("unchecked") + @Test + @DisplayName("Returns vanilla block drops when entity is null") + void drops_returnsVanillaDrops_whenEntityIsNull() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + ItemStack tool = mock(ItemStack.class); + ItemStack dropItem = mock(ItemStack.class); + Collection drops = List.of(dropItem); + + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(null); + when(block.getDrops(tool, null)).thenReturn(drops); + + List result = hook.drops(block, tool, null); + + assertEquals(1, result.size()); + assertTrue(result.contains(dropItem)); + } + } + } + + @Nested + @DisplayName("playBlockDropEffects") + class PlayBlockDropEffectsTests { + + @Mock + private Block block; + + @Test + @DisplayName("Plays custom block effects when block is custom") + void playBlockDropEffects_playsCustomEffects_whenCustomBlock() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + CustomBlock customBlock = mock(CustomBlock.class); + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(customBlock); + + hook.playBlockDropEffects(block); + + verify(customBlock).playBreakEffect(); + verify(customBlock).playBreakSound(); + verify(customBlock).playBreakParticles(); + } + } + + @Test + @DisplayName("Plays vanilla block effects when block is not custom") + void playBlockDropEffects_playsVanillaEffects_whenNotCustomBlock() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(null); + + World world = mock(World.class); + BlockData blockData = mock(BlockData.class); + SoundGroup soundGroup = mock(SoundGroup.class); + Location location = mock(Location.class); + + when(block.getWorld()).thenReturn(world); + when(block.getBlockData()).thenReturn(blockData); + when(block.getBlockSoundGroup()).thenReturn(soundGroup); + when(block.getLocation()).thenReturn(location); + when(location.clone()).thenReturn(location); + when(location.add(0.5, 0.5, 0.5)).thenReturn(location); + when(soundGroup.getBreakSound()).thenReturn(org.bukkit.Sound.BLOCK_STONE_BREAK); + when(soundGroup.getVolume()).thenReturn(1.0f); + when(soundGroup.getPitch()).thenReturn(1.0f); + + hook.playBlockDropEffects(block); + + verify(world).playSound(eq(location), eq(org.bukkit.Sound.BLOCK_STONE_BREAK), eq(1.0f), eq(1.0f)); + verify(world).spawnParticle(eq(Particle.BLOCK), eq(location), eq(20), + eq(0.25), eq(0.25), eq(0.25), eq(0.05), eq(blockData)); + } + } + } + + @Nested + @DisplayName("removeBlock") + class RemoveBlockTests { + + @Mock + private Block block; + + @Test + @DisplayName("Removes custom block when it is a custom block and removal succeeds") + void removeBlock_removesCustomBlock_whenSuccessful() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + CustomBlock customBlock = mock(CustomBlock.class); + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(customBlock); + when(customBlock.remove()).thenReturn(true); + + hook.removeBlock(block); + + verify(customBlock).remove(); + } + } + + @Test + @DisplayName("Throws when custom block removal fails") + void removeBlock_throwsIllegalState_whenRemovalFails() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + CustomBlock customBlock = mock(CustomBlock.class); + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(customBlock); + when(customBlock.remove()).thenReturn(false); + when(customBlock.getModelPath()).thenReturn("ns:failing_block"); + Location loc = mock(Location.class); + when(block.getLocation()).thenReturn(loc); + + assertThrows(IllegalStateException.class, () -> hook.removeBlock(block)); + } + } + + @Test + @DisplayName("Sets block to AIR when it is not a custom block") + void removeBlock_setsAir_whenNotCustomBlock() { + try (MockedStatic mocked = mockStatic(CustomBlock.class)) { + mocked.when(() -> CustomBlock.byAlreadyPlaced(block)).thenReturn(null); + + hook.removeBlock(block); + + verify(block).setType(Material.AIR); + } + } + } + + @Nested + @DisplayName("itemName") + class ItemNameTests { + + @Test + @DisplayName("Returns Adventure component name when available") + void itemName_returnsComponentName_whenAvailable() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.getInstance("ns:my_item")).thenReturn(customStack); + when(customStack.itemName()).thenReturn(Component.text("Fancy Sword")); + + CustomItemWrapper wrapper = new TestCustomItemWrapper("ns:my_item"); + String result = hook.itemName(wrapper); + + assertEquals("Fancy Sword", result); + } + } + + @Test + @DisplayName("Falls back to legacy display name when component name is null") + void itemName_returnsLegacyName_whenComponentNull() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.getInstance("ns:my_item")).thenReturn(customStack); + when(customStack.itemName()).thenReturn(null); + when(customStack.getDisplayName()).thenReturn("§6Golden Blade"); + + CustomItemWrapper wrapper = new TestCustomItemWrapper("ns:my_item"); + String result = hook.itemName(wrapper); + + assertEquals("Golden Blade", result); + } + } + + @Test + @DisplayName("Falls back to legacy display name when component name is empty") + void itemName_returnsLegacyName_whenComponentEmpty() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.getInstance("ns:my_item")).thenReturn(customStack); + when(customStack.itemName()).thenReturn(Component.empty()); + when(customStack.getDisplayName()).thenReturn("Legacy Name"); + + CustomItemWrapper wrapper = new TestCustomItemWrapper("ns:my_item"); + String result = hook.itemName(wrapper); + + assertEquals("Legacy Name", result); + } + } + + @Test + @DisplayName("Falls back to formatted block ID when both names are unavailable") + void itemName_returnsFormattedId_whenBothNamesUnavailable() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.getInstance("ns:cool_item")).thenReturn(customStack); + when(customStack.itemName()).thenReturn(null); + when(customStack.getDisplayName()).thenReturn(null); + + CustomItemWrapper wrapper = new TestCustomItemWrapper("ns:cool_item"); + String result = hook.itemName(wrapper); + + assertEquals("Cool Item", result); + } + } + + @Test + @DisplayName("Falls back to formatted block ID when legacy name is empty") + void itemName_returnsFormattedId_whenLegacyNameEmpty() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.getInstance("ns:cool_item")).thenReturn(customStack); + when(customStack.itemName()).thenReturn(null); + when(customStack.getDisplayName()).thenReturn(""); + + CustomItemWrapper wrapper = new TestCustomItemWrapper("ns:cool_item"); + String result = hook.itemName(wrapper); + + assertEquals("Cool Item", result); + } + } + + @Test + @DisplayName("Falls back to formatted block ID when CustomStack is null") + void itemName_returnsFormattedId_whenCustomStackNull() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + mocked.when(() -> CustomStack.getInstance("ns:cool_item")).thenReturn(null); + + CustomItemWrapper wrapper = new TestCustomItemWrapper("ns:cool_item"); + String result = hook.itemName(wrapper); + + assertEquals("Cool Item", result); + } + } + + @Test + @DisplayName("Returns formatted material name when wrapper has no custom item") + void itemName_returnsFormattedMaterial_whenVanilla() { + CustomItemWrapper wrapper = new TestVanillaItemWrapper(Material.OAK_LOG); + String result = hook.itemName(wrapper); + + assertEquals("Oak Log", result); + } + } + + @Nested + @DisplayName("blockName") + class BlockNameTests { + + @Test + @DisplayName("Returns Adventure component name when available") + void blockName_returnsComponentName_whenAvailable() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.getInstance("ns:my_block")).thenReturn(customStack); + when(customStack.itemName()).thenReturn(Component.text("Fancy Block")); + + CustomBlockWrapper wrapper = new TestCustomBlockWrapper("ns:my_block"); + String result = hook.blockName(wrapper); + + assertEquals("Fancy Block", result); + } + } + + @Test + @DisplayName("Falls back to legacy display name when component name is null") + void blockName_returnsLegacyName_whenComponentNull() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.getInstance("ns:my_block")).thenReturn(customStack); + when(customStack.itemName()).thenReturn(null); + when(customStack.getDisplayName()).thenReturn("§aGreen Block"); + + CustomBlockWrapper wrapper = new TestCustomBlockWrapper("ns:my_block"); + String result = hook.blockName(wrapper); + + assertEquals("Green Block", result); + } + } + + @Test + @DisplayName("Falls back to legacy display name when component name is empty") + void blockName_returnsLegacyName_whenComponentEmpty() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.getInstance("ns:my_block")).thenReturn(customStack); + when(customStack.itemName()).thenReturn(Component.empty()); + when(customStack.getDisplayName()).thenReturn("Legacy Block"); + + CustomBlockWrapper wrapper = new TestCustomBlockWrapper("ns:my_block"); + String result = hook.blockName(wrapper); + + assertEquals("Legacy Block", result); + } + } + + @Test + @DisplayName("Falls back to formatted block ID when legacy name is empty") + void blockName_returnsFormattedId_whenLegacyNameEmpty() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.getInstance("ns:cool_block")).thenReturn(customStack); + when(customStack.itemName()).thenReturn(null); + when(customStack.getDisplayName()).thenReturn(""); + + CustomBlockWrapper wrapper = new TestCustomBlockWrapper("ns:cool_block"); + String result = hook.blockName(wrapper); + + assertEquals("Cool Block", result); + } + } + + @Test + @DisplayName("Falls back to formatted block ID when both names are unavailable") + void blockName_returnsFormattedId_whenBothNamesUnavailable() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + CustomStack customStack = mock(CustomStack.class); + mocked.when(() -> CustomStack.getInstance("ns:cool_block")).thenReturn(customStack); + when(customStack.itemName()).thenReturn(null); + when(customStack.getDisplayName()).thenReturn(null); + + CustomBlockWrapper wrapper = new TestCustomBlockWrapper("ns:cool_block"); + String result = hook.blockName(wrapper); + + assertEquals("Cool Block", result); + } + } + + @Test + @DisplayName("Falls back to formatted block ID when CustomStack is null") + void blockName_returnsFormattedId_whenCustomStackNull() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + mocked.when(() -> CustomStack.getInstance("ns:cool_block")).thenReturn(null); + + CustomBlockWrapper wrapper = new TestCustomBlockWrapper("ns:cool_block"); + String result = hook.blockName(wrapper); + + assertEquals("Cool Block", result); + } + } + + @Test + @DisplayName("Returns formatted material name when wrapper has no custom block") + void blockName_returnsFormattedMaterial_whenVanilla() { + CustomBlockWrapper wrapper = new TestVanillaBlockWrapper(Material.STONE_BRICKS); + String result = hook.blockName(wrapper); + + assertEquals("Stone Bricks", result); + } + + @Test + @DisplayName("Formats block ID without namespace correctly") + void blockName_formatsIdWithoutNamespace() { + try (MockedStatic mocked = mockStatic(CustomStack.class)) { + mocked.when(() -> CustomStack.getInstance("simple_block")).thenReturn(null); + + CustomBlockWrapper wrapper = new TestCustomBlockWrapper("simple_block"); + String result = hook.blockName(wrapper); + + assertEquals("Simple Block", result); + } + } + } + + private static class TestCustomItemWrapper extends CustomItemWrapper { + + private final String testCustomItem; + + TestCustomItemWrapper(@NotNull String customItem) { + super(Material.STONE); + this.testCustomItem = customItem; + } + + @NotNull + @Override + public Optional customItem() { + return Optional.of(testCustomItem); + } + + @NotNull + @Override + public Optional material() { + return Optional.empty(); + } + } + + private static class TestVanillaItemWrapper extends CustomItemWrapper { + + private final Material testMaterial; + + TestVanillaItemWrapper(@NotNull Material material) { + super(material); + this.testMaterial = material; + } + + @NotNull + @Override + public Optional customItem() { + return Optional.empty(); + } + + @NotNull + @Override + public Optional material() { + return Optional.of(testMaterial); + } + } + + private static class TestCustomBlockWrapper extends CustomBlockWrapper { + + private final String testCustomBlock; + + TestCustomBlockWrapper(@NotNull String customBlock) { + super(Material.STONE); + this.testCustomBlock = customBlock; + } + + @NotNull + @Override + public Optional customBlock() { + return Optional.of(testCustomBlock); + } + + @NotNull + @Override + public Optional material() { + return Optional.empty(); + } + } + + private static class TestVanillaBlockWrapper extends CustomBlockWrapper { + + private final Material testMaterial; + + TestVanillaBlockWrapper(@NotNull Material material) { + super(material); + this.testMaterial = material; + } + + @NotNull + @Override + public Optional customBlock() { + return Optional.empty(); + } + + @NotNull + @Override + public Optional material() { + return Optional.of(testMaterial); + } + } +}