"""
Unit tests for the crafting system contrib.

"""

from unittest import mock

from django.core.exceptions import ObjectDoesNotExist
from django.test import override_settings

from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.create import create_object
from evennia.utils.test_resources import BaseEvenniaTestCase

from . import crafting, example_recipes


class TestCraftUtils(BaseEvenniaTestCase):
    """
    Test helper utils for crafting.

    """

    maxDiff = None

    @override_settings(CRAFT_RECIPE_MODULES=[])
    def test_load_recipes(self):
        """This should only load the example module now"""

        crafting._load_recipes()
        self.assertEqual(
            crafting._RECIPE_CLASSES,
            {
                "crucible steel": example_recipes.CrucibleSteelRecipe,
                "leather": example_recipes.LeatherRecipe,
                "fireball": example_recipes.FireballRecipe,
                "heal": example_recipes.HealingRecipe,
                "oak bark": example_recipes.OakBarkRecipe,
                "pig iron": example_recipes.PigIronRecipe,
                "rawhide": example_recipes.RawhideRecipe,
                "sword": example_recipes.SwordRecipe,
                "sword blade": example_recipes.SwordBladeRecipe,
                "sword guard": example_recipes.SwordGuardRecipe,
                "sword handle": example_recipes.SwordHandleRecipe,
                "sword pommel": example_recipes.SwordPommelRecipe,
            },
        )


class _TestMaterial:
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return self.name


class TestCraftingRecipeBase(BaseEvenniaTestCase):
    """
    Test the parent recipe class.
    """

    def setUp(self):
        self.crafter = mock.MagicMock()
        self.crafter.msg = mock.MagicMock()

        self.inp1 = _TestMaterial("test1")
        self.inp2 = _TestMaterial("test2")
        self.inp3 = _TestMaterial("test3")

        self.kwargs = {"kw1": 1, "kw2": 2}

        self.recipe = crafting.CraftingRecipeBase(
            self.crafter, self.inp1, self.inp2, self.inp3, **self.kwargs
        )

    def test_msg(self):
        """Test messaging to crafter"""

        self.recipe.msg("message")
        self.crafter.msg.assert_called_with(text=("message", {"type": "crafting"}))

    def test_pre_craft(self):
        """Test validating hook"""
        self.recipe.pre_craft()
        self.assertEqual(self.recipe.validated_inputs, (self.inp1, self.inp2, self.inp3))

    def test_pre_craft_fail(self):
        """Should rase error if validation fails"""
        self.recipe.allow_craft = False
        with self.assertRaises(crafting.CraftingValidationError):
            self.recipe.pre_craft()

    def test_craft_hook__succeed(self):
        """Test craft hook, the main access method."""

        expected_result = _TestMaterial("test_result")
        self.recipe.do_craft = mock.MagicMock(return_value=expected_result)

        self.assertTrue(self.recipe.allow_craft)

        result = self.recipe.craft()

        # check result
        self.assertEqual(result, expected_result)
        self.recipe.do_craft.assert_called_with(kw1=1, kw2=2)

        # since allow_reuse is False, this usage should now be turned off
        self.assertFalse(self.recipe.allow_craft)
        # trying to re-run again should fail since rerun is False
        with self.assertRaises(crafting.CraftingError):
            self.recipe.craft()

    def test_craft_hook__fail(self):
        """Test failing the call"""

        self.recipe.do_craft = mock.MagicMock(return_value=None)

        # trigger exception
        with self.assertRaises(crafting.CraftingError):
            self.recipe.craft(raise_exception=True)

        # reset and try again without exception
        self.recipe.allow_craft = True
        result = self.recipe.craft()
        self.assertEqual(result, None)


class _MockRecipe(crafting.CraftingRecipe):
    name = "testrecipe"
    tool_tags = ["tool1", "tool2"]
    consumable_tags = ["cons1", "cons2", "cons3"]
    output_prototypes = [
        {
            "key": "Result1",
            "prototype_key": "resultprot",
            "tags": [("result1", "crafting_material")],
        }
    ]


@override_settings(CRAFT_RECIPE_MODULES=[])
class TestCraftingRecipe(BaseEvenniaTestCase):
    """
    Test the CraftingRecipe class with one recipe
    """

    maxDiff = None

    def setUp(self):
        self.crafter = mock.MagicMock()
        self.crafter.msg = mock.MagicMock()

        self.tool1 = create_object(key="tool1", tags=[("tool1", "crafting_tool")], nohome=True)
        self.tool2 = create_object(key="tool2", tags=[("tool2", "crafting_tool")], nohome=True)
        self.cons1 = create_object(key="cons1", tags=[("cons1", "crafting_material")], nohome=True)
        self.cons2 = create_object(key="cons2", tags=[("cons2", "crafting_material")], nohome=True)
        self.cons3 = create_object(key="cons3", tags=[("cons3", "crafting_material")], nohome=True)

    def tearDown(self):
        try:
            self.tool1.delete()
            self.tool2.delete()
            self.cons1.delete()
            self.cons2.delete()
            self.cons3.delete()
        except ObjectDoesNotExist:
            pass

    def test_error_format(self):
        """Test the automatic error formatter"""
        recipe = _MockRecipe(
            self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3
        )

        msg = "{missing},{tools},{consumables},{inputs},{outputs}" "{i0},{i1},{o0}"
        kwargs = {
            "missing": "foo",
            "tools": ["bar", "bar2", "bar3"],
            "consumables": ["cons1", "cons2"],
        }

        expected = {
            "missing": "foo",
            "i0": "cons1",
            "i1": "cons2",
            "i2": "cons3",
            "o0": "Result1",
            "tools": "bar, bar2, and bar3",
            "consumables": "cons1 and cons2",
            "inputs": "cons1, cons2, and cons3",
            "outputs": "Result1",
        }

        result = recipe._format_message(msg, **kwargs)
        self.assertEqual(result, msg.format_map(expected))

    def test_craft__success(self):
        """Test to create a result from the recipe"""
        recipe = _MockRecipe(
            self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3
        )

        result = recipe.craft()

        self.assertEqual(result[0].key, "Result1")
        self.assertEqual(result[0].tags.all(), ["result1", "resultprot"])
        self.crafter.msg.assert_called_with(
            text=(recipe.success_message.format(outputs="Result1"), {"type": "crafting"})
        )

        # make sure consumables are gone
        self.assertIsNone(self.cons1.pk)
        self.assertIsNone(self.cons2.pk)
        self.assertIsNone(self.cons3.pk)
        # make sure tools remain
        self.assertIsNotNone(self.tool1.pk)
        self.assertIsNotNone(self.tool2.pk)

    def test_seed__success(self):
        """Test seed helper classmethod"""

        # needed for other dbs to pass seed
        homeroom = create_object(key="HomeRoom", nohome=True)

        # call classmethod directly
        with override_settings(DEFAULT_HOME=f"#{homeroom.id}"):
            tools, consumables = _MockRecipe.seed()

        # this should be a normal successful crafting
        recipe = _MockRecipe(self.crafter, *(tools + consumables))

        result = recipe.craft()

        self.assertEqual(result[0].key, "Result1")
        self.assertEqual(result[0].tags.all(), ["result1", "resultprot"])
        self.crafter.msg.assert_called_with(
            text=(recipe.success_message.format(outputs="Result1"), {"type": "crafting"})
        )

        # make sure consumables are gone
        for cons in consumables:
            self.assertIsNone(cons.pk)
        # make sure tools remain
        for tool in tools:
            self.assertIsNotNone(tool.pk)

    def test_craft_missing_tool__fail(self):
        """Fail craft by missing tool2"""
        recipe = _MockRecipe(self.crafter, self.tool1, self.cons1, self.cons2, self.cons3)
        result = recipe.craft()
        self.assertFalse(result)
        self.crafter.msg.assert_called_with(
            text=(
                recipe.error_tool_missing_message.format(outputs="Result1", missing="tool2"),
                {"type": "crafting"},
            )
        )

        # make sure consumables are still there
        self.assertIsNotNone(self.cons1.pk)
        self.assertIsNotNone(self.cons2.pk)
        self.assertIsNotNone(self.cons3.pk)
        # make sure tools remain
        self.assertIsNotNone(self.tool1.pk)
        self.assertIsNotNone(self.tool2.pk)

    def test_craft_missing_cons__fail(self):
        """Fail craft by missing cons3"""
        recipe = _MockRecipe(self.crafter, self.tool1, self.tool2, self.cons1, self.cons2)
        result = recipe.craft()
        self.assertFalse(result)
        self.crafter.msg.assert_called_with(
            text=(
                recipe.error_consumable_missing_message.format(outputs="Result1", missing="cons3"),
                {"type": "crafting"},
            )
        )

        # make sure consumables are still there
        self.assertIsNotNone(self.cons1.pk)
        self.assertIsNotNone(self.cons2.pk)
        self.assertIsNotNone(self.cons3.pk)
        # make sure tools remain
        self.assertIsNotNone(self.tool1.pk)
        self.assertIsNotNone(self.tool2.pk)

    def test_craft_missing_cons__always_consume__fail(self):
        """Fail craft by missing cons3, with always-consume flag"""

        cons4 = create_object(key="cons4", tags=[("cons4", "crafting_material")], nohome=True)

        recipe = _MockRecipe(self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, cons4)
        recipe.consume_on_fail = True

        result = recipe.craft()

        self.assertFalse(result)
        self.crafter.msg.assert_called_with(
            text=(
                recipe.error_consumable_missing_message.format(outputs="Result1", missing="cons3"),
                {"type": "crafting"},
            )
        )

        # make sure consumables are deleted even though we failed
        self.assertIsNone(self.cons1.pk)
        self.assertIsNone(self.cons2.pk)
        # the extra should also be gone
        self.assertIsNone(cons4.pk)
        # but cons3 should be fine since it was not included
        self.assertIsNotNone(self.cons3.pk)
        # make sure tools remain as normal
        self.assertIsNotNone(self.tool1.pk)
        self.assertIsNotNone(self.tool2.pk)

    def test_craft_wrong_tool__fail(self):
        """Fail craft by including a wrong tool"""

        wrong = create_object(key="wrong", tags=[("wrongtool", "crafting_tool")], nohome=True)

        recipe = _MockRecipe(self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, wrong)
        result = recipe.craft()
        self.assertFalse(result)
        self.crafter.msg.assert_called_with(
            text=(
                recipe.error_tool_excess_message.format(
                    outputs="Result1", excess=wrong.get_display_name(looker=self.crafter)
                ),
                {"type": "crafting"},
            )
        )
        # make sure consumables are still there
        self.assertIsNotNone(self.cons1.pk)
        self.assertIsNotNone(self.cons2.pk)
        self.assertIsNotNone(self.cons3.pk)
        # make sure tools remain
        self.assertIsNotNone(self.tool1.pk)
        self.assertIsNotNone(self.tool2.pk)

    def test_craft_tool_excess__fail(self):
        """Fail by too many consumables"""

        # note that this is a valid tag!
        tool3 = create_object(key="tool3", tags=[("tool2", "crafting_tool")], nohome=True)

        recipe = _MockRecipe(
            self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, tool3
        )
        result = recipe.craft()
        self.assertFalse(result)
        self.crafter.msg.assert_called_with(
            text=(
                recipe.error_tool_excess_message.format(
                    outputs="Result1", excess=tool3.get_display_name(looker=self.crafter)
                ),
                {"type": "crafting"},
            )
        )

        # make sure consumables are still there
        self.assertIsNotNone(self.cons1.pk)
        self.assertIsNotNone(self.cons2.pk)
        self.assertIsNotNone(self.cons3.pk)
        # make sure tools remain
        self.assertIsNotNone(self.tool1.pk)
        self.assertIsNotNone(self.tool2.pk)
        self.assertIsNotNone(tool3.pk)

    def test_craft_cons_excess__fail(self):
        """Fail by too many consumables"""

        # note that this is a valid tag!
        cons4 = create_object(key="cons4", tags=[("cons3", "crafting_material")], nohome=True)

        recipe = _MockRecipe(
            self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, cons4
        )
        result = recipe.craft()
        self.assertFalse(result)
        self.crafter.msg.assert_called_with(
            text=(
                recipe.error_consumable_excess_message.format(
                    outputs="Result1", excess=cons4.get_display_name(looker=self.crafter)
                ),
                {"type": "crafting"},
            )
        )

        # make sure consumables are still there
        self.assertIsNotNone(self.cons1.pk)
        self.assertIsNotNone(self.cons2.pk)
        self.assertIsNotNone(self.cons3.pk)
        self.assertIsNotNone(cons4.pk)
        # make sure tools remain
        self.assertIsNotNone(self.tool1.pk)
        self.assertIsNotNone(self.tool2.pk)

    def test_craft_tool_excess__sucess(self):
        """Allow too many consumables"""

        tool3 = create_object(key="tool3", tags=[("tool2", "crafting_tool")], nohome=True)

        recipe = _MockRecipe(
            self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, tool3
        )
        recipe.exact_tools = False
        result = recipe.craft()
        self.assertTrue(result)
        self.crafter.msg.assert_called_with(
            text=(recipe.success_message.format(outputs="Result1"), {"type": "crafting"})
        )

        # make sure consumables are gone
        self.assertIsNone(self.cons1.pk)
        self.assertIsNone(self.cons2.pk)
        self.assertIsNone(self.cons3.pk)
        # make sure tools remain
        self.assertIsNotNone(self.tool1.pk)
        self.assertIsNotNone(self.tool2.pk)

    def test_craft_cons_excess__sucess(self):
        """Allow too many consumables"""

        cons4 = create_object(key="cons4", tags=[("cons3", "crafting_material")], nohome=True)

        recipe = _MockRecipe(
            self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, cons4
        )
        recipe.exact_consumables = False
        result = recipe.craft()
        self.assertTrue(result)
        self.crafter.msg.assert_called_with(
            text=(recipe.success_message.format(outputs="Result1"), {"type": "crafting"})
        )

        # make sure consumables are gone
        self.assertIsNone(self.cons1.pk)
        self.assertIsNone(self.cons2.pk)
        self.assertIsNone(self.cons3.pk)
        # make sure tools remain
        self.assertIsNotNone(self.tool1.pk)
        self.assertIsNotNone(self.tool2.pk)

    def test_craft_tool_order__fail(self):
        """Strict tool-order recipe fail"""
        recipe = _MockRecipe(
            self.crafter, self.tool2, self.tool1, self.cons1, self.cons2, self.cons3
        )
        recipe.exact_tool_order = True
        result = recipe.craft()
        self.assertFalse(result)
        self.crafter.msg.assert_called_with(
            text=(
                recipe.error_tool_order_message.format(
                    outputs="Result1", missing=self.tool2.get_display_name(looker=self.crafter)
                ),
                {"type": "crafting"},
            )
        )

        # make sure consumables are still there
        self.assertIsNotNone(self.cons1.pk)
        self.assertIsNotNone(self.cons2.pk)
        self.assertIsNotNone(self.cons3.pk)
        # make sure tools remain
        self.assertIsNotNone(self.tool1.pk)
        self.assertIsNotNone(self.tool2.pk)

    def test_craft_cons_order__fail(self):
        """Strict tool-order recipe fail"""
        recipe = _MockRecipe(
            self.crafter, self.tool1, self.tool2, self.cons3, self.cons2, self.cons1
        )
        recipe.exact_consumable_order = True
        result = recipe.craft()
        self.assertFalse(result)
        self.crafter.msg.assert_called_with(
            text=(
                recipe.error_consumable_order_message.format(
                    outputs="Result1", missing=self.cons3.get_display_name(looker=self.crafter)
                ),
                {"type": "crafting"},
            )
        )

        # make sure consumables are still there
        self.assertIsNotNone(self.cons1.pk)
        self.assertIsNotNone(self.cons2.pk)
        self.assertIsNotNone(self.cons3.pk)
        # make sure tools remain
        self.assertIsNotNone(self.tool1.pk)
        self.assertIsNotNone(self.tool2.pk)


class TestCraftSword(BaseEvenniaTestCase):
    """
    Test the `craft` function by crafting the example sword.

    """

    def setUp(self):
        self.crafter = mock.MagicMock()
        self.crafter.msg = mock.MagicMock()

    @override_settings(CRAFT_RECIPE_MODULES=[], DEFAULT_HOME="#999999")
    @mock.patch("evennia.contrib.game_systems.crafting.example_recipes.random")
    def test_craft_sword(self, mockrandom):
        """
        Craft example sword. For the test, every crafting works.

        """
        # make sure every craft succeeds
        mockrandom.random = mock.MagicMock(return_value=0.2)

        def _co(key, tagkey, is_tool=False):
            tagcat = "crafting_tool" if is_tool else "crafting_material"
            return create_object(key=key, tags=[(tagkey, tagcat)], nohome=True)

        def _craft(recipe_name, *inputs):
            """shortcut to shorten and return only one element"""
            result = crafting.craft(self.crafter, recipe_name, *inputs, raise_exception=True)
            return result[0] if len(result) == 1 else result

        # generate base materials
        iron_ore1 = _co("Iron ore ingot", "iron ore")
        iron_ore2 = _co("Iron ore ingot", "iron ore")
        iron_ore3 = _co("Iron ore ingot", "iron ore")

        ash1 = _co("Pile of Ash", "ash")
        ash2 = _co("Pile of Ash", "ash")
        ash3 = _co("Pile of Ash", "ash")

        sand1 = _co("Pile of sand", "sand")
        sand2 = _co("Pile of sand", "sand")
        sand3 = _co("Pile of sand", "sand")

        coal01 = _co("Pile of coal", "coal")
        coal02 = _co("Pile of coal", "coal")
        coal03 = _co("Pile of coal", "coal")
        coal04 = _co("Pile of coal", "coal")
        coal05 = _co("Pile of coal", "coal")
        coal06 = _co("Pile of coal", "coal")
        coal07 = _co("Pile of coal", "coal")
        coal08 = _co("Pile of coal", "coal")
        coal09 = _co("Pile of coal", "coal")
        coal10 = _co("Pile of coal", "coal")
        coal11 = _co("Pile of coal", "coal")
        coal12 = _co("Pile of coal", "coal")

        oak_wood = _co("Pile of oak wood", "oak wood")
        water = _co("Bucket of water", "water")
        fur = _co("Bundle of Animal fur", "fur")

        # tools
        blast_furnace = _co("Blast furnace", "blast furnace", is_tool=True)
        furnace = _co("Smithing furnace", "furnace", is_tool=True)
        crucible = _co("Smelting crucible", "crucible", is_tool=True)
        anvil = _co("Smithing anvil", "anvil", is_tool=True)
        hammer = _co("Smithing hammer", "hammer", is_tool=True)
        knife = _co("Working knife", "knife", is_tool=True)
        cauldron = _co("Cauldron", "cauldron", is_tool=True)

        # making pig iron
        inputs = [iron_ore1, coal01, coal02, blast_furnace]
        pig_iron1 = _craft("pig iron", *inputs)

        inputs = [iron_ore2, coal03, coal04, blast_furnace]
        pig_iron2 = _craft("pig iron", *inputs)

        inputs = [iron_ore3, coal05, coal06, blast_furnace]
        pig_iron3 = _craft("pig iron", *inputs)

        # making crucible steel
        inputs = [pig_iron1, ash1, sand1, coal07, coal08, crucible]
        crucible_steel1 = _craft("crucible steel", *inputs)

        inputs = [pig_iron2, ash2, sand2, coal09, coal10, crucible]
        crucible_steel2 = _craft("crucible steel", *inputs)

        inputs = [pig_iron3, ash3, sand3, coal11, coal12, crucible]
        crucible_steel3 = _craft("crucible steel", *inputs)

        # smithing
        inputs = [crucible_steel1, hammer, anvil, furnace]
        sword_blade = _craft("sword blade", *inputs)

        inputs = [crucible_steel2, hammer, anvil, furnace]
        sword_pommel = _craft("sword pommel", *inputs)

        inputs = [crucible_steel3, hammer, anvil, furnace]
        sword_guard = _craft("sword guard", *inputs)

        # stripping fur
        inputs = [fur, knife]
        rawhide = _craft("rawhide", *inputs)

        # making bark (tannin) and cleaned wood
        inputs = [oak_wood, knife]
        oak_bark, cleaned_oak_wood = _craft("oak bark", *inputs)

        # leathermaking
        inputs = [rawhide, oak_bark, water, cauldron]
        leather = _craft("leather", *inputs)

        # sword handle
        inputs = [cleaned_oak_wood, knife]
        sword_handle = _craft("sword handle", *inputs)

        # sword (order matters)
        inputs = [
            sword_blade,
            sword_guard,
            sword_pommel,
            sword_handle,
            leather,
            knife,
            hammer,
            furnace,
        ]
        sword = _craft("sword", *inputs)

        self.assertEqual(sword.key, "Sword")

        # make sure all materials and intermediaries are deleted
        self.assertIsNone(iron_ore1.pk)
        self.assertIsNone(iron_ore2.pk)
        self.assertIsNone(iron_ore3.pk)
        self.assertIsNone(ash1.pk)
        self.assertIsNone(ash2.pk)
        self.assertIsNone(ash3.pk)
        self.assertIsNone(sand1.pk)
        self.assertIsNone(sand2.pk)
        self.assertIsNone(sand3.pk)
        self.assertIsNone(coal01.pk)
        self.assertIsNone(coal02.pk)
        self.assertIsNone(coal03.pk)
        self.assertIsNone(coal04.pk)
        self.assertIsNone(coal05.pk)
        self.assertIsNone(coal06.pk)
        self.assertIsNone(coal07.pk)
        self.assertIsNone(coal08.pk)
        self.assertIsNone(coal09.pk)
        self.assertIsNone(coal10.pk)
        self.assertIsNone(coal11.pk)
        self.assertIsNone(coal12.pk)
        self.assertIsNone(oak_wood.pk)
        self.assertIsNone(water.pk)
        self.assertIsNone(fur.pk)
        self.assertIsNone(pig_iron1.pk)
        self.assertIsNone(pig_iron2.pk)
        self.assertIsNone(pig_iron3.pk)
        self.assertIsNone(crucible_steel1.pk)
        self.assertIsNone(crucible_steel2.pk)
        self.assertIsNone(crucible_steel3.pk)
        self.assertIsNone(sword_blade.pk)
        self.assertIsNone(sword_pommel.pk)
        self.assertIsNone(sword_guard.pk)
        self.assertIsNone(rawhide.pk)
        self.assertIsNone(oak_bark.pk)
        self.assertIsNone(leather.pk)
        self.assertIsNone(sword_handle.pk)

        # make sure all tools remain
        self.assertIsNotNone(blast_furnace)
        self.assertIsNotNone(furnace)
        self.assertIsNotNone(crucible)
        self.assertIsNotNone(anvil)
        self.assertIsNotNone(hammer)
        self.assertIsNotNone(knife)
        self.assertIsNotNone(cauldron)


@mock.patch("evennia.contrib.game_systems.crafting.crafting._load_recipes", new=mock.MagicMock())
@mock.patch(
    "evennia.contrib.game_systems.crafting.crafting._RECIPE_CLASSES",
    new={"testrecipe": _MockRecipe},
)
@override_settings(CRAFT_RECIPE_MODULES=[])
class TestCraftCommand(BaseEvenniaCommandTest):
    """Test the crafting command"""

    def setUp(self):
        super().setUp()

        tools, consumables = _MockRecipe.seed(
            tool_kwargs={"location": self.char1}, consumable_kwargs={"location": self.char1}
        )

    def test_craft__success(self):
        "Successfully craft using command"
        self.call(
            crafting.CmdCraft(),
            "testrecipe from cons1, cons2, cons3 using tool1, tool2",
            _MockRecipe.success_message.format(outputs="Result1"),
        )

    def test_craft__notools__failure(self):
        "Craft fail no tools"
        self.call(
            crafting.CmdCraft(),
            "testrecipe from cons1, cons2, cons3",
            _MockRecipe.error_tool_missing_message.format(outputs="Result1", missing="tool1"),
        )

    def test_craft__nocons__failure(self):
        self.call(
            crafting.CmdCraft(),
            "testrecipe using tool1, tool2",
            _MockRecipe.error_consumable_missing_message.format(outputs="Result1", missing="cons1"),
        )

    def test_craft__unknown_recipe__failure(self):
        self.call(
            crafting.CmdCraft(),
            "nonexistent from cons1, cons2, cons3 using tool1, tool2",
            "Unknown recipe 'nonexistent'",
        )
