# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Tests for the unified marketplace catalog, entities, and validation.
"""

import json
import tempfile
from pathlib import Path
from unittest.mock import patch

from familiar.marketplace.entities import (
    CatalogEntry,
    EntityType,
    FamiliarBundle,
    JobEntry,
    SkillEntry,
    SpeciesEntry,
    ToolEntry,
)

# ── Test Entity Types ───────────────────────────────────────────────


class TestEntityTypes:
    """Test entity dataclasses and EntityType enum."""

    def test_entity_type_values(self):
        assert EntityType.FAMILIAR.value == "familiar"
        assert EntityType.SPECIES.value == "species"
        assert EntityType.JOB.value == "job"
        assert EntityType.SKILL.value == "skill"
        assert EntityType.TOOL.value == "tool"

    def test_entity_type_count(self):
        assert len(EntityType) == 5

    def test_catalog_entry_creation(self):
        entry = CatalogEntry(
            id="fox",
            entity_type=EntityType.SPECIES,
            name="Fox",
            version="1.0.0",
            author="Familiar",
            description="Quick, clever, warm",
            tags=["species", "fox"],
            source="builtin",
        )
        assert entry.id == "fox"
        assert entry.entity_type == EntityType.SPECIES
        assert entry.name == "Fox"
        assert entry.source == "builtin"

    def test_catalog_entry_to_dict(self):
        entry = CatalogEntry(
            id="owl",
            entity_type=EntityType.SPECIES,
            name="Owl",
            version="1.0.0",
            author="Familiar",
            description="Wise",
            tags=["species"],
        )
        d = entry.to_dict()
        assert d["id"] == "owl"
        assert d["entity_type"] == "species"
        assert d["name"] == "Owl"
        assert isinstance(d["tags"], list)

    def test_catalog_entry_from_dict_roundtrip(self):
        original = CatalogEntry(
            id="cat",
            entity_type=EntityType.SPECIES,
            name="Cat",
            version="2.0.0",
            author="Test",
            description="Independent",
            tags=["species", "cat"],
            familiar_version=">=1.14.60",
            source="custom",
            entity_data={"key": "cat", "label": "Cat"},
        )
        d = original.to_dict()
        restored = CatalogEntry.from_dict(d)
        assert restored.id == original.id
        assert restored.entity_type == original.entity_type
        assert restored.name == original.name
        assert restored.version == original.version
        assert restored.author == original.author
        assert restored.tags == original.tags
        assert restored.familiar_version == original.familiar_version
        assert restored.source == original.source
        assert restored.entity_data == original.entity_data

    def test_catalog_entry_matches_id(self):
        entry = CatalogEntry(
            id="fox",
            entity_type=EntityType.SPECIES,
            name="Fox",
            version="1.0.0",
            author="",
            description="Quick",
        )
        assert entry.matches("fox")
        assert entry.matches("FOX")

    def test_catalog_entry_matches_name(self):
        entry = CatalogEntry(
            id="helper",
            entity_type=EntityType.JOB,
            name="Helper",
            version="1.0.0",
            author="",
            description="General",
        )
        assert entry.matches("help")

    def test_catalog_entry_matches_description(self):
        entry = CatalogEntry(
            id="wolf",
            entity_type=EntityType.SPECIES,
            name="Wolf",
            version="1.0.0",
            author="",
            description="Pack-minded",
        )
        assert entry.matches("pack")

    def test_catalog_entry_matches_tags(self):
        entry = CatalogEntry(
            id="cal",
            entity_type=EntityType.SKILL,
            name="Calendar",
            version="1.0.0",
            author="",
            description="Schedule",
            tags=["productivity"],
        )
        assert entry.matches("productivity")

    def test_catalog_entry_no_match(self):
        entry = CatalogEntry(
            id="fox",
            entity_type=EntityType.SPECIES,
            name="Fox",
            version="1.0.0",
            author="",
            description="Quick",
        )
        assert not entry.matches("nonexistent")

    def test_species_entry(self):
        se = SpeciesEntry(
            key="phoenix",
            label="Phoenix",
            description="Fiery",
            personality="Rebirth",
            baby_preview=[" /~\\", "(o.o)", " \\_/"],
            voice_directive="You are fierce",
        )
        assert se.key == "phoenix"
        assert se.voice_directive == "You are fierce"

    def test_job_entry(self):
        je = JobEntry(
            key="helper",
            name="Helper",
            icon="fa-hands-helping",
            description="General purpose",
            prompt_fragment="You help.",
        )
        assert je.key == "helper"
        assert je.skill_weights == {}

    def test_skill_entry(self):
        se = SkillEntry(
            key="calendar",
            name="Calendar",
            description="Schedule mgmt",
            tool_names=["calendar_today"],
        )
        assert se.tool_names == ["calendar_today"]

    def test_tool_entry(self):
        te = ToolEntry(name="calendar_today", description="Get today's events", skill="calendar")
        assert te.skill == "calendar"

    def test_familiar_bundle(self):
        fb = FamiliarBundle(
            name="Wellness Coach",
            species_key="phoenix",
            job_key="wellness_coach",
            skill_keys=["fitness"],
            icon="fa-heart-pulse",
            author="Test",
        )
        assert fb.species_key == "phoenix"
        assert fb.skill_keys == ["fitness"]


# ── Test Marketplace Catalog ────────────────────────────────────────


class TestMarketplaceCatalog:
    """Test MarketplaceCatalog CRUD, browse, search, stats, export."""

    def _make_entry(self, id_: str, etype: EntityType, **kwargs):
        defaults = {
            "name": id_.title(),
            "version": "1.0.0",
            "author": "Test",
            "description": f"Test {id_}",
            "tags": [etype.value, id_],
            "source": "builtin",
        }
        defaults.update(kwargs)
        return CatalogEntry(id=id_, entity_type=etype, **defaults)

    def test_add_and_get(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog(auto_seed=False)
        entry = self._make_entry("fox", EntityType.SPECIES)
        cat.add(entry)
        result = cat.get("species", "fox")
        assert result is not None
        assert result.id == "fox"

    def test_remove(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog(auto_seed=False)
        cat.add(self._make_entry("fox", EntityType.SPECIES))
        assert cat.remove("species", "fox") is True
        assert cat.get("species", "fox") is None

    def test_remove_nonexistent(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog(auto_seed=False)
        assert cat.remove("species", "nope") is False

    def test_get_nonexistent(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog(auto_seed=False)
        assert cat.get("species", "nope") is None

    def test_browse_all(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog(auto_seed=False)
        cat.add(self._make_entry("fox", EntityType.SPECIES))
        cat.add(self._make_entry("helper", EntityType.JOB))
        results = cat.browse()
        assert len(results) == 2

    def test_browse_by_type(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog(auto_seed=False)
        cat.add(self._make_entry("fox", EntityType.SPECIES))
        cat.add(self._make_entry("helper", EntityType.JOB))
        results = cat.browse(entity_type="species")
        assert len(results) == 1
        assert results[0].id == "fox"

    def test_browse_by_tags(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog(auto_seed=False)
        cat.add(self._make_entry("fox", EntityType.SPECIES, tags=["species", "cute"]))
        cat.add(self._make_entry("owl", EntityType.SPECIES, tags=["species", "wise"]))
        results = cat.browse(tags=["cute"])
        assert len(results) == 1
        assert results[0].id == "fox"

    def test_browse_by_source(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog(auto_seed=False)
        cat.add(self._make_entry("fox", EntityType.SPECIES, source="builtin"))
        cat.add(self._make_entry("phoenix", EntityType.SPECIES, source="custom"))
        results = cat.browse(source="custom")
        assert len(results) == 1
        assert results[0].id == "phoenix"

    def test_browse_pagination(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog(auto_seed=False)
        for i in range(10):
            cat.add(self._make_entry(f"skill_{i:02d}", EntityType.SKILL))
        page1 = cat.browse(limit=3, offset=0)
        page2 = cat.browse(limit=3, offset=3)
        assert len(page1) == 3
        assert len(page2) == 3
        assert page1[0].id != page2[0].id

    def test_search(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog(auto_seed=False)
        cat.add(self._make_entry("fox", EntityType.SPECIES, description="Quick, clever, warm"))
        cat.add(self._make_entry("owl", EntityType.SPECIES, description="Wise, observant"))
        results = cat.search("clever")
        assert len(results) == 1
        assert results[0].id == "fox"

    def test_search_by_type(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog(auto_seed=False)
        cat.add(self._make_entry("fox", EntityType.SPECIES))
        cat.add(self._make_entry("fox_skill", EntityType.SKILL, description="Fox-related"))
        results = cat.search("fox", entity_type="species")
        assert len(results) == 1
        assert results[0].entity_type == EntityType.SPECIES

    def test_stats_empty(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog(auto_seed=False)
        s = cat.stats()
        assert s == {"familiar": 0, "species": 0, "job": 0, "skill": 0, "tool": 0}

    def test_stats_with_entries(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog(auto_seed=False)
        cat.add(self._make_entry("fox", EntityType.SPECIES))
        cat.add(self._make_entry("owl", EntityType.SPECIES))
        cat.add(self._make_entry("helper", EntityType.JOB))
        s = cat.stats()
        assert s["species"] == 2
        assert s["job"] == 1

    def test_export_catalog(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog(auto_seed=False)
        cat.add(self._make_entry("fox", EntityType.SPECIES))
        export = cat.export_catalog()
        assert "species" in export
        assert "fox" in export["species"]
        assert export["species"]["fox"]["name"] == "Fox"


# ── Test Catalog Seeding ────────────────────────────────────────────


class TestCatalogSeeding:
    """Test that seeding populates from built-in registries."""

    def test_seed_returns_counts(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog(auto_seed=False)
        counts = cat.seed()
        assert "species" in counts
        assert "job" in counts
        assert "skill" in counts
        assert "tool" in counts
        assert "familiar" in counts

    def test_seed_species_count(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog(auto_seed=False)
        counts = cat.seed()
        assert counts["species"] == 6

    def test_seed_jobs_count(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog(auto_seed=False)
        counts = cat.seed()
        assert counts["job"] == 6

    def test_seed_finds_skills(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog(auto_seed=False)
        counts = cat.seed()
        assert counts["skill"] > 0

    def test_seed_finds_tools(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog(auto_seed=False)
        counts = cat.seed()
        assert counts["tool"] > 0

    def test_seeded_species_have_entity_data(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog()
        fox = cat.get("species", "fox")
        assert fox is not None
        assert fox.entity_data.get("key") == "fox"
        assert fox.entity_data.get("label") == "Fox"
        assert fox.entity_data.get("voice_directive") != ""
        assert fox.entity_data.get("baby_preview") is not None

    def test_seeded_jobs_have_entity_data(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog()
        helper = cat.get("job", "helper")
        assert helper is not None
        assert helper.entity_data.get("key") == "helper"
        assert helper.entity_data.get("name") == "Helper"

    def test_seed_idempotent(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog(auto_seed=False)
        counts1 = cat.seed()
        counts2 = cat.seed()
        assert counts1 == counts2
        # Still same number of entries (not doubled)
        stats = cat.stats()
        assert stats["species"] == 6

    def test_all_six_species_present(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog()
        for key in ("fox", "owl", "cat", "dragon", "wolf", "frog"):
            entry = cat.get("species", key)
            assert entry is not None, f"Missing species: {key}"

    def test_all_six_jobs_present(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        cat = MarketplaceCatalog()
        expected = {
            "helper",
            "social_worker",
            "business_buddy",
            "chef",
            "artist",
            "nonprofit_director",
        }
        stats = cat.stats()
        assert stats["job"] >= 6
        for key in expected:
            assert cat.get("job", key) is not None, f"Missing job: {key}"

    def test_custom_bundles_from_disk(self):
        from familiar.marketplace.catalog import MarketplaceCatalog

        with tempfile.TemporaryDirectory() as tmp:
            bundles_dir = Path(tmp) / "custom_bundles"
            bundles_dir.mkdir()

            bundle_data = {
                "schema_version": "1.0",
                "type": "familiar",
                "metadata": {"id": "test_bundle", "name": "Test Bundle", "version": "1.0.0"},
                "bundle": {
                    "name": "Test Bundle",
                    "species_key": "fox",
                    "job_key": "helper",
                    "description": "A test bundle",
                },
            }
            (bundles_dir / "test_bundle.json").write_text(json.dumps(bundle_data))

            cat = MarketplaceCatalog(auto_seed=False)
            with patch("familiar.core.paths.DATA_DIR", Path(tmp)):
                count = cat._load_custom_bundles()

            assert count == 1
            entry = cat.get("familiar", "test_bundle")
            assert entry is not None
            assert entry.source == "custom"


# ── Test Species Registry ──────────────────────────────────────────


class TestSpeciesRegistry:
    """Test species extensibility via the registry pattern."""

    def test_builtin_species_in_registry(self):
        from familiar.creature.species import SPECIES_REGISTRY

        assert len(SPECIES_REGISTRY) >= 6
        for key in ("fox", "owl", "cat", "dragon", "wolf", "frog"):
            assert key in SPECIES_REGISTRY

    def test_register_species(self):
        from familiar.creature.species import (
            SPECIES_ORDER,
            SPECIES_REGISTRY,
            SpeciesInfo,
            register_species,
        )

        info = SpeciesInfo(
            key="phoenix_test",
            label="Phoenix Test",
            description="Fiery test",
            personality="Rebirth",
            baby_preview=[" /~\\", "(o.o)", " \\_/"],
        )
        register_species(info)
        try:
            assert "phoenix_test" in SPECIES_REGISTRY
            assert "phoenix_test" in SPECIES_ORDER
            assert SPECIES_REGISTRY["phoenix_test"].label == "Phoenix Test"
        finally:
            SPECIES_REGISTRY.pop("phoenix_test", None)
            if "phoenix_test" in SPECIES_ORDER:
                SPECIES_ORDER.remove("phoenix_test")

    def test_get_species_info(self):
        from familiar.creature.species import get_species_info

        fox = get_species_info("fox")
        assert fox is not None
        assert fox.label == "Fox"

    def test_get_species_info_none(self):
        from familiar.creature.species import get_species_info

        assert get_species_info("nonexistent") is None

    def test_list_all_species(self):
        from familiar.creature.species import list_all_species

        all_sp = list_all_species()
        assert len(all_sp) >= 6
        keys = [s.key for s in all_sp]
        assert "fox" in keys
        assert "frog" in keys

    def test_custom_species_from_json(self):
        from familiar.creature.species import (
            SPECIES_ORDER,
            SPECIES_REGISTRY,
            SPECIES_VOICE_DIRECTIVES,
        )

        with tempfile.TemporaryDirectory() as tmp:
            custom_dir = Path(tmp) / "custom_species"
            custom_dir.mkdir()

            sp_data = {
                "species": {
                    "key": "unicorn_test",
                    "label": "Unicorn Test",
                    "description": "Magical test creature",
                    "personality": "Sparkly",
                    "baby_preview": [" /|", "(o )", " \\/"],
                    "voice_directive": "You sparkle with magic.",
                },
            }
            (custom_dir / "unicorn_test.json").write_text(json.dumps(sp_data))

            with patch("familiar.core.paths.DATA_DIR", Path(tmp)):
                from familiar.creature.species import _load_custom_species

                _load_custom_species()

            try:
                assert "unicorn_test" in SPECIES_REGISTRY
                assert SPECIES_REGISTRY["unicorn_test"].label == "Unicorn Test"
                assert SPECIES_VOICE_DIRECTIVES.get("unicorn_test") == "You sparkle with magic."
            finally:
                SPECIES_REGISTRY.pop("unicorn_test", None)
                SPECIES_VOICE_DIRECTIVES.pop("unicorn_test", None)
                if "unicorn_test" in SPECIES_ORDER:
                    SPECIES_ORDER.remove("unicorn_test")

    def test_custom_voice_directive(self):
        from familiar.creature.species import (
            SPECIES_ORDER,
            SPECIES_REGISTRY,
            SPECIES_VOICE_DIRECTIVES,
            SpeciesInfo,
            register_species,
        )

        info = SpeciesInfo(
            key="robot_test",
            label="Robot Test",
            description="Beep boop",
            personality="Logical",
            baby_preview=["[o]", "|H|", " | "],
        )
        register_species(info)
        SPECIES_VOICE_DIRECTIVES["robot_test"] = "You speak in precise, logical terms."
        try:
            from familiar.creature.species import get_species_voice

            voice = get_species_voice("robot_test")
            assert "precise" in voice
        finally:
            SPECIES_REGISTRY.pop("robot_test", None)
            SPECIES_VOICE_DIRECTIVES.pop("robot_test", None)
            if "robot_test" in SPECIES_ORDER:
                SPECIES_ORDER.remove("robot_test")


# ── Test Custom Sprites ─────────────────────────────────────────────


class TestCustomSprites:
    """Test custom sprite registration and lookup."""

    def test_register_custom_sprites(self):
        from familiar.creature.sprites import CUSTOM_SPRITES, register_custom_sprites

        sprites = {
            "baby": {"idle": [[[".", "#", "."], ["#", "B", "#"], [".", "#", "."]]]},
        }
        register_custom_sprites("test_species", sprites)
        try:
            assert "test_species" in CUSTOM_SPRITES
        finally:
            CUSTOM_SPRITES.pop("test_species", None)

    def test_custom_sprites_used_in_lookup(self):
        from familiar.creature.sprites import (
            CUSTOM_SPRITES,
            get_sprite_frames,
            register_custom_sprites,
        )

        custom_frame = [["X", "X"], ["X", "X"]]
        sprites = {"baby": {"idle": [custom_frame]}}
        register_custom_sprites("custom_test", sprites)
        try:
            frames = get_sprite_frames("custom_test", "baby", "idle")
            assert frames == [custom_frame]
        finally:
            CUSTOM_SPRITES.pop("custom_test", None)

    def test_fallback_still_works(self):
        from familiar.creature.sprites import get_sprite_frames

        frames = get_sprite_frames("nonexistent_species", "baby", "idle")
        assert len(frames) > 0  # falls back to fox


# ── Test Validation ─────────────────────────────────────────────────


class TestValidation:
    """Test package validation for all entity types."""

    def test_valid_species_package(self):
        from familiar.marketplace.validation import validate_package

        pkg = {
            "schema_version": "1.0",
            "type": "species",
            "metadata": {"id": "phoenix", "name": "Phoenix", "version": "1.0.0"},
            "species": {
                "key": "phoenix",
                "label": "Phoenix",
                "description": "Fiery",
                "personality": "Rebirth",
                "baby_preview": [" /~\\"],
            },
        }
        errors = validate_package(pkg)
        assert errors == []

    def test_species_missing_fields(self):
        from familiar.marketplace.validation import validate_package

        pkg = {
            "schema_version": "1.0",
            "type": "species",
            "metadata": {"id": "phoenix", "name": "Phoenix", "version": "1.0.0"},
            "species": {"key": "phoenix"},
        }
        errors = validate_package(pkg)
        assert any("species.label" in e for e in errors)
        assert any("species.description" in e for e in errors)

    def test_species_missing_section(self):
        from familiar.marketplace.validation import validate_package

        pkg = {
            "schema_version": "1.0",
            "type": "species",
            "metadata": {"id": "phoenix", "name": "Phoenix", "version": "1.0.0"},
        }
        errors = validate_package(pkg)
        assert any("species" in e.lower() for e in errors)

    def test_valid_bundle_package(self):
        from familiar.marketplace.validation import validate_package

        pkg = {
            "schema_version": "1.0",
            "type": "familiar",
            "metadata": {"id": "wellness", "name": "Wellness Coach", "version": "1.0.0"},
            "bundle": {"name": "Wellness Coach", "species_key": "phoenix"},
        }
        errors = validate_package(pkg)
        assert errors == []

    def test_bundle_missing_name(self):
        from familiar.marketplace.validation import validate_package

        pkg = {
            "schema_version": "1.0",
            "type": "familiar",
            "metadata": {"id": "wellness", "name": "Wellness Coach", "version": "1.0.0"},
            "bundle": {"species_key": "phoenix"},
        }
        errors = validate_package(pkg)
        assert any("bundle.name" in e for e in errors)

    def test_compatibility_pass(self):
        from familiar.marketplace.validation import check_compatibility

        pkg = {"metadata": {"familiar_version": ">=1.0.0"}}
        assert check_compatibility(pkg) is None

    def test_compatibility_fail(self):
        from familiar.marketplace.validation import check_compatibility

        pkg = {"metadata": {"familiar_version": ">=99.99.99"}}
        result = check_compatibility(pkg)
        assert result is not None
        assert "99.99.99" in result

    def test_compatibility_no_constraint(self):
        from familiar.marketplace.validation import check_compatibility

        pkg = {"metadata": {}}
        assert check_compatibility(pkg) is None

    def test_invalid_type(self):
        from familiar.marketplace.validation import validate_package

        pkg = {
            "schema_version": "1.0",
            "type": "unknown",
            "metadata": {"id": "x", "name": "X", "version": "1.0.0"},
        }
        errors = validate_package(pkg)
        assert any("Invalid type" in e for e in errors)

    def test_missing_schema_version(self):
        from familiar.marketplace.validation import validate_package

        pkg = {"type": "species", "metadata": {"id": "x", "name": "X", "version": "1.0.0"}}
        errors = validate_package(pkg)
        assert any("schema_version" in e for e in errors)

    def test_invalid_version_format(self):
        from familiar.marketplace.validation import validate_package

        pkg = {
            "schema_version": "1.0",
            "type": "species",
            "metadata": {"id": "x", "name": "X", "version": "bad"},
            "species": {
                "key": "x",
                "label": "X",
                "description": "X",
                "personality": "X",
                "baby_preview": ["X"],
            },
        }
        errors = validate_package(pkg)
        assert any("version format" in e.lower() for e in errors)

    def test_valid_types_includes_species_and_familiar(self):
        from familiar.marketplace.validation import _VALID_TYPES

        assert "species" in _VALID_TYPES
        assert "familiar" in _VALID_TYPES
        assert "job_class" in _VALID_TYPES
        assert "skill" in _VALID_TYPES
        assert "bundle" in _VALID_TYPES


# ── Test Jobs Marketplace Valid Types ────────────────────────────────


class TestJobsMarketplaceTypes:
    """Test that jobs/_marketplace.py also has extended types."""

    def test_valid_types_extended(self):
        from familiar.jobs._marketplace import _VALID_TYPES

        assert "species" in _VALID_TYPES
        assert "familiar" in _VALID_TYPES
