"""Category classification.

Feed categories are the strongest signal -- many bottles never say what they are
('Johnnie Walker Blue Label' contains no word for whisky) -- but a feed's own
strings include navigation labels and packaging words that are not categories a
shopper browses by.
"""

import pytest

from app.services.taxonomy import CATEGORY_RULES, classify

CANONICAL = {name for name, _ in CATEGORY_RULES}


class TestFeedHints:
    def test_a_feed_category_wins(self):
        assert classify("Johnnie Walker Blue Label", "Johnnie Walker", ["Whisky"]) == "Whisky"

    @pytest.mark.parametrize("junk", ["View All", "Summer Drinks", "Mix Match", "Brumas"])
    def test_navigation_labels_never_become_categories(self, junk):
        """These leaked into the category dropdown as browsable categories."""
        assert classify("Unknown Product", None, [junk]) is None

    @pytest.mark.parametrize("packaging", ["Bottle", "Gift Box", "Tin", "Miniature"])
    def test_packaging_is_not_a_category(self, packaging):
        assert classify("Unknown Product", None, [packaging]) is None


class TestFromName:
    @pytest.mark.parametrize(
        ("name", "expected"),
        [
            ("Glenfiddich 12 Year Old Single Malt", "Whisky"),
            ("Hennessy XO", "Cognac & Brandy"),
            ("Patron Silver Tequila", "Tequila & Mezcal"),
            ("Bombay Sapphire Gin", "Gin"),
            ("Moet & Chandon Brut Champagne", "Champagne & Sparkling"),
            ("Baileys Irish Cream Liqueur", "Liqueurs & Aperitifs"),
        ],
    )
    def test_classifies(self, name, expected):
        assert classify(name, None, []) == expected

    def test_returns_a_canonical_category_or_none(self):
        """Never a raw feed string: everything shown as a category is one we chose."""
        result = classify("Some Mystery Item", None, ["Wholly Unknown Label"])
        assert result is None or result in CANONICAL

    def test_no_category_beats_a_wrong_one(self):
        assert classify("Assorted Gift Selection", None, []) is None


class TestFamilyLabel:
    """The shopper's word for a category's family travels with each CategoryCount
    (`family` + `family_label`), so the mega menu groups shelves without a copy of
    the taxonomy in the SPA."""

    def test_every_category_has_a_family_and_every_family_a_label(self):
        from app.services.taxonomy import FAMILY_LABEL, VERTICAL_OF_CATEGORY, family_label

        for category, vertical in VERTICAL_OF_CATEGORY.items():
            assert vertical in FAMILY_LABEL, f"{category!r} maps to {vertical!r}, which has no label"
            assert family_label(category) == FAMILY_LABEL[vertical]
        assert CANONICAL <= set(VERTICAL_OF_CATEGORY), CANONICAL - set(VERTICAL_OF_CATEGORY)

    def test_the_words_a_shopper_reads(self):
        from app.services.taxonomy import family_label

        assert family_label("Whisky") == "Drinks"
        assert family_label("Perfume") == "Beauty"
        assert family_label("Confectionery") == "Confectionery"
        assert family_label("Tobacco") == "Tobacco"

    def test_an_unclassified_or_unknown_category_has_no_family(self):
        from app.models.schemas import CategoryCount
        from app.services.taxonomy import family_label

        assert family_label(None) is None
        assert family_label("View All") is None
        # A count built the old way is unchanged: both fields default to None.
        count = CategoryCount(category="Whisky", count=21)
        assert (count.family, count.family_label) == (None, None)


class TestPerfumeIsTheCategoryWord:
    def test_the_shopper_reads_perfume_never_fragrance(self):
        """Structure review, 9 Sep: "duty free perfume" has 19x the search
        volume of "duty free fragrance" (Mark) and "Search is always our
        driver" (Adam). Fragrance had been the category on 2,219 rows; the
        rename is `backfill perfume` after deploy, and this pins the rule so
        a new collector cannot reintroduce the trade word."""
        from app.services.taxonomy import VERTICAL_OF_CATEGORY, classify, vertical_of

        assert classify("Chanel No 5 Eau de Parfum 100ml", "Chanel", []) == "Perfume"
        assert classify("Dior Sauvage Eau de Toilette 60ml", "Dior", []) == "Perfume"
        assert vertical_of("Perfume") == "beauty"
        assert "Fragrance" not in VERTICAL_OF_CATEGORY
        assert vertical_of("Fragrance") is None


class TestMarkedTail:
    """A variant the shop marked after " / " never classifies (identity rules v5, the catalogue
    decisions §2.6): three lipsticks whose shades are called praline nude, Chocolate and Toffee
    Talk were filed under Confectionery on the 15 Sep staging copy."""

    def test_a_shade_named_after_a_sweet_is_not_confectionery(self):
        assert classify("Joli Rouge Velvet 3.5 gr / 784V praline nude", "CLARINS") != "Confectionery"
        assert classify("Lip Comfort Oil 7 ml / 09 Chocolate", "CLARINS") != "Confectionery"
        assert classify("Lip Idole Juicytreat Gloss 9 ml / 25 Toffee Talk", "Lancome") != "Confectionery"
        assert classify("Rouge Allure Lipstick 3.5 gr / 99 Pirate", "CHANEL") == "Makeup"

    def test_a_flavour_before_the_separator_still_classifies(self):
        assert classify("Lindor Milk Chocolate Truffles 200 g / Milk", "Lindt") == "Confectionery"

