"""Size, barcode and identity parsing.

Each case below is a real record that was stored wrong at some point.
"""

import pytest

from app.services.normalize import (
    clean_gtin,
    looks_exclusive,
    match_key,
    parse_abv,
    parse_size_ml,
    parse_size_ml_from_url,
    gtin_from_sku,
)


class TestSizeFromName:
    @pytest.mark.parametrize(
        ("name", "expected"),
        [
            # Multipacks stored the UNIT size: a case of beer became one bottle.
            ("Amstel 24x330ml", 7920),
            ("Bombay Sapphire Twin Pack 2x1L", 2000),
            ("Grant's Buy 2 Get 1 Free 3x1L", 3000),
            ("Heineken 24 Bottles x 330ml", 7920),
            ("Corona 6 Cans x 355ml", 2130),
            # A source typo: "0.70cl" means 0.70 L, and a 7 ml spirit does not exist.
            ("Glenfiddich 12 Year Old 0.70cl", 700),
            # Ordinary forms.
            ("Johnnie Walker Blue Label 1L", 1000),
            ("Baileys 70cl", 700),
            ("Hendrick's Gin 700ml", 700),
            ("Bombay Sapphire 1.75 Litre", 1750),
            ("Miniature 50ml", 50),
            ("No size stated here", None),
        ],
    )
    def test_parses(self, name, expected):
        assert parse_size_ml(name) == expected

    def test_rejects_implausible_sizes(self):
        assert parse_size_ml("Tank 900L") is None


class TestSizeFromUrl:
    """Avolta tile names omit the size their URL slug states; without this
    fallback the same bottle cannot merge across shops."""

    @pytest.mark.parametrize(
        ("url", "expected"),
        [
            ("https://athens.shopdutyfree.com/en/137/jw-blue-label-1l-2184884p1137004", 1000),
            ("https://x.com/en/1/glenfiddich-0-7l-12345p678", 700),
            # Hyphens stand in for the decimal point AND separate the unit.
            ("https://www.bordershop.com/x/hans-baer-riesling-white-0-25-l/p/0001774176/", 250),
            ("https://x.com/en/1/some-bottle-2x1l-999p1", 2000),
            # Slugs drop the decimal point: "-15l" is the 1.5 litre, proven by
            # the page's own "#1.5l" anchor. Read as 15 litres, it compared a
            # magnum at one shop against the 75cl at another.
            ("https://zurich.shopdutyfree.com/en/53/armand-de-brignac-gold-15l", 1500),
            ("https://x.com/en/1/bottega-gold-15l", 1500),
            ("https://x.com/en/1/big-jug-3l", 3000),
            ("https://x.com/en/1/gordons-1-75l-123p9", 1750),
            ("https://x.com/en/1/no-size-here-12345p678", None),
            (None, None),
        ],
    )
    def test_parses(self, url, expected):
        assert parse_size_ml_from_url(url) == expected

    def test_ignores_the_host(self):
        # A digit-and-letter run in the domain must not read as a size.
        assert parse_size_ml_from_url("https://shop7l.example.com/en/1/bottle") is None


class TestBarcodes:
    def test_accepts_a_real_gtin(self):
        assert clean_gtin("5000267023625") == "5000267023625"

    def test_normalises_padding(self):
        """The same barcode arrives padded to different widths per retailer."""
        assert clean_gtin("0005000267023625") == clean_gtin("5000267023625")

    def test_rejects_a_sequential_sku(self):
        """Two shops numbering their own products from 1 would otherwise 'match'
        unrelated bottles. The check digit is what separates the two."""
        assert clean_gtin("821398") is None

    @pytest.mark.parametrize("code", ["2058860740008", "0203456789019"])
    def test_rejects_restricted_circulation_codes(self, code):
        """GS1 reserves 02 and 2x for company-internal use, so the same code can
        sit on different bottles at different retailers. One such code paired a
        limited edition with the regular bottle."""
        assert clean_gtin(code) is None

    @pytest.mark.parametrize("junk", [None, "", "N/A", "12345"])
    def test_rejects_junk(self, junk):
        assert clean_gtin(junk) is None


class TestMatchKey:
    def test_brand_repeated_in_name_collapses(self):
        """('Johnnie Walker', 'Johnnie Walker Blue Label') and
        ('Johnnie Walker', 'Blue Label') are the same bottle."""
        assert match_key("Johnnie Walker", "Johnnie Walker Blue Label", 1000) == match_key(
            "Johnnie Walker", "Blue Label", 1000
        )

    def test_size_separates_variants(self):
        assert match_key("Bombay", "Sapphire", 1000) != match_key("Bombay", "Sapphire", 700)

    def test_packaging_noise_is_ignored(self):
        assert match_key("Moet", "Brut Imperial Gift Box", 750) == match_key(
            "Moet", "Brut Imperial", 750
        )


class TestMisc:
    @pytest.mark.parametrize(
        ("text", "expected"),
        [("Whisky 40%", 40.0), ("Gin 47.3%", 47.3), ("No strength", None), ("Bogus 250%", None)],
    )
    def test_abv(self, text, expected):
        assert parse_abv(text) == expected

    def test_exclusive_from_name(self):
        """Five of nine retailers publish no exclusivity flag, so the name is the
        only signal that a bottle is travel-retail only."""
        assert looks_exclusive("Talisker Dark Storm Travel Exclusive 1L")
        assert looks_exclusive("Chivas Airport Exclusive")
        assert not looks_exclusive("Talisker 10 Year Old 70cl")


class TestEntityHygiene:
    def test_ingest_boundary_decodes_entities(self):
        """Feeds ship "Bacard&iacute;"; stored verbatim it renders literally on
        every page. The decode lives at the one ingest boundary every collector
        passes through -- this pins the rule."""
        import html
        assert html.unescape("Bacard&iacute; 8 Year Old") == "Bacardí 8 Year Old"
        assert html.unescape("Mo&euml;t &amp; Chandon") == "Moët & Chandon"


class TestGtinFromSku:
    """A supplier code is only a barcode if it already is one.

    The failure this guards against is not theoretical: 119 stored barcodes
    were short SKUs zero-padded to 13 that happened to pass the check digit.
    """

    def test_real_ean13_in_a_sku_field_is_accepted(self):
        # Ardbeg 10, corroborated by a second retailer.
        assert gtin_from_sku("5010494195125") == "5010494195125"

    def test_magento_style_short_sku_is_refused(self):
        # 2997278 padded to 13 is 0000002997278, whose check digit passes.
        # clean_gtin accepts it; a SKU field must not.
        assert clean_gtin("0000002997278") == "0000002997278"
        assert gtin_from_sku("2997278") is None

    def test_nine_and_ten_digit_codes_are_refused(self):
        # Not a GTIN width, so it cannot be a barcode however it validates.
        assert gtin_from_sku("093575553") is None
        assert gtin_from_sku("0093575553") is None

    def test_upc12_is_accepted(self):
        assert gtin_from_sku("012345678905") is not None

    def test_gtin8_is_accepted_at_its_own_width(self):
        assert gtin_from_sku("50196081") is not None

    def test_a_sku_with_punctuation_still_reads_as_digits(self):
        assert gtin_from_sku("5-010494-195125") == "5010494195125"

    def test_none_and_text_are_refused(self):
        assert gtin_from_sku(None) is None
        assert gtin_from_sku("ABC-123") is None
