"""The quantity parser and the v4 key slot, pinned on the real staging rows (Stream L).

Every case is a live name from the 14 Sep copy with its stored (size_value, size_unit) as the
hint, and what it cost: 7293 "Dior Sauvage Stick Deodorant 75g" was stored as 75 ml and keyed
beside the 75 ml bottles; 14386 "Benefit Pop, Lock & Lash It 4g + 3.5ml" was stored as 35 ml;
3,416 products keyed on `|0` and 65 key groups collided there. Under v4 every slot carries its
unit and unknown is a word (rian, 15 Sep).
"""

import pytest

from app.services.normalize import match_key, size_is_implausible
from app.services.quantity import (
    CEILING_ML,
    Quantity,
    as_quantity,
    from_stored,
    parse_quantity,
    quantity_key,
)


def slot(text, **kw):
    return quantity_key(parse_quantity(text, **kw))


class TestPrecedence:
    def test_a_stored_hint_never_beats_a_gram_in_the_text(self):
        """7293: stored 75 ml for a 75 g stick. The text names the dimension."""
        q = parse_quantity("Dior Sauvage Stick Deodorant 75g", hint=(75, "ml"))
        assert (q.value, q.unit, q.state, q.detail) == (75, "g", "stated", "hint_disagrees")
        assert quantity_key(q) == "75g"

    def test_a_stored_hint_never_beats_a_set_in_the_text(self):
        """14386: stored 35 ml for a 4 g + 3.5 ml duo."""
        q = parse_quantity("Benefit Pop, Lock & Lash It 4g + 3.5ml", hint=(35, "ml"))
        assert q.form == "set" and q.set_contents == "3.5ml+4g" and q.value is None and q.unit is None
        assert quantity_key(q) == "3.5ml+4g|set"

    def test_a_rounded_stored_hint_never_beats_the_exact_text(self):
        """1583 "0.375L": the stored pair had been rounded to (0.38, "l")."""
        q = parse_quantity("Vodka 0.375L", hint=(0.38, "l"))
        assert (q.value, quantity_key(q)) == (375, "375ml")

    def test_a_structured_field_beats_the_text_in_the_same_dimension(self):
        q = parse_quantity("Glenfiddich 12 Year Old 750ml", hint=(700, "ml"), hint_rank="structured")
        assert (q.value, q.unit, q.detail) == (700, "ml", "text_disagrees")

    def test_a_structured_field_loses_to_a_different_dimension_a_set_or_a_pack(self):
        assert parse_quantity("Stick 75g", hint=(75, "ml"), hint_rank="structured").unit == "g"
        assert parse_quantity("Duo 50ml + 100ml", hint=(150, "ml"), hint_rank="structured").form == "set"
        assert parse_quantity("2 x 1L", hint=(2000, "ml"), hint_rank="structured").pack_count == 2

    def test_the_hint_fills_a_silent_name(self):
        q = parse_quantity("Sauvage", hint=(100, "ml"))
        assert (q.value, q.unit, q.state, q.form) == (100, "ml", "stated", "single")
        assert quantity_key(parse_quantity("Sauvage")) == "unknown"

    def test_a_silent_set_takes_no_figure_from_a_hint(self):
        q = parse_quantity("Mini Travel Set", hint=(50, "ml"))
        assert quantity_key(q) == "unknown|set" and q.state == "none"


class TestSetsAndPacks:
    def test_every_part_kept_and_sorted_a_pack_inside_a_set_expanded(self):
        assert slot("Daywear Set 50ml + 2x15ml + 5ml") == "5ml+15ml+15ml+50ml|set"
        assert slot("Kérastase set 250ml + 200ml + 150ml") == "150ml+200ml+250ml|set"
        q = parse_quantity("Kérastase set 250ml + 200ml + 150ml")
        assert (q.value, q.unit, q.ml) == (600, "ml", 600)

    def test_the_one_million_duo_keys_on_its_contents(self):
        assert slot("Rabanne Set: Duo 1 Million Eau de Toilette 50 ml 100 ml", hint=(50, "ml")) == "50ml+100ml|set"
        assert slot("Gift Set 100ml") == "100ml|set"

    def test_a_bare_piece_count_on_a_set_is_recorded_but_never_keys_it(self):
        q = parse_quantity("4 Pieces Mini Travel Set")
        assert (q.value, q.unit, q.state) == (4, "pcs", "none")
        assert quantity_key(q) == "unknown|set"

    def test_a_pack_keys_on_its_total_in_any_dimension(self):
        q = parse_quantity("24 x 330ml")
        assert (q.value, q.pack_count, q.pack_unit_value, quantity_key(q)) == (7920, 24, 330, "7920ml|pack")
        assert slot("24 x 20g") == "480g|pack"
        assert slot("Chivas Regal 12y 40% 2x1L") == "2000ml|pack"

    def test_a_glued_multiplier_is_a_count_not_a_pack(self):
        q = parse_quantity("Biscuits X20 Pieces")
        assert (q.value, q.unit, q.form, quantity_key(q)) == (20, "pcs", "single", "20pcs")

    def test_a_pack_with_an_extra_is_the_pack_and_a_glued_suffix_hides_only_the_figure(self):
        assert slot("Absolut Twin Pack 2x1L With Free Absolut Raspberri 375ml") == "2000ml|pack"
        assert slot("Royal Export 5.8% 24x0.33LDS", hint=(7920, "ml")) == "7920ml|pack"

    def test_a_refill(self):
        assert slot("Fame Refill 200ml") == "200ml|refill"


class TestOunces:
    @pytest.mark.parametrize("name, expected", [
        ("Pure Musc 3.3 Oz 5cl", 50), ("Pure Musc 3.3 Oz 10cl", 100),
        ("/1.7floz 5cl", 50), ("/1.7floz 10cl", 100), ("/2.7oz 5cl", 50), ("/2.7oz 8cl", 80),
    ])
    def test_metric_beats_imperial(self, name, expected):
        """7965/7966, 9051/9052, 10729/10730."""
        assert parse_quantity(name).ml == expected

    def test_fl_oz_is_always_millilitres_and_snaps_to_the_nominal_bottle(self):
        assert parse_quantity("1.7 fl oz").value == 50 and parse_quantity("1 fl oz").value == 30
        assert slot("1.7 fl oz") == "50ml"
        assert parse_quantity("Amo Flowerful Eau de Parfum 3.4 Oz (women)", category="Perfume").value == 100

    def test_a_bare_oz_is_grams_for_a_cream_or_a_bar_and_millilitres_for_a_scent(self):
        assert (parse_quantity("Moisture Surge 1 oz", category="Skincare").value,
                parse_quantity("Moisture Surge 1 oz", category="Skincare").unit) == (30, "g")
        assert parse_quantity("Bar 3.5 oz", category="Confectionery").unit == "g"
        assert parse_quantity("Eau Fraiche 3.4 oz", category="Perfume").unit == "ml"
        assert parse_quantity("Whisky 25.4 oz", category="Whisky").unit == "ml"


class TestTheSlot:
    def test_millilitres_round_the_way_v3_did(self):
        """17911, 17500, 15772: banker's rounding through normalize.size_ml_of, never int()."""
        assert slot("Something 92.5 ml") == "92ml"
        assert slot("Something 37.5 ml") == "38ml"
        assert slot("Something 11.5 ml") == "12ml"
        assert parse_quantity("Something 92.5 ml").value == 92.5

    def test_counts_and_grams(self):
        assert slot("200pcs") == "200pcs"
        q = parse_quantity("Advanced Ceramide Capsules 90 caps")
        assert (q.value, q.unit) == (90, "pcs")
        assert slot("Macarons 72 g") == "72g" and slot("Bar 2.5g") == "2.5g" and slot("Sack 1 kg") == "1000g"

    def test_unknown_is_a_word_and_a_pound_is_seen_but_not_understood(self):
        assert quantity_key(None) == "unknown"
        assert parse_quantity("Ham 2 lb").state == "unparsed" and slot("Ham 2 lb") == "unknown"
        assert parse_quantity("Nothing at all").state == "none"

    def test_a_litre_never_reads_the_l_of_leau_and_a_typo_centilitre_is_a_litre(self):
        assert slot("N°5 L'EAU 100ml") == "100ml"
        assert slot("Chivas 0.70cl") == "700ml"

    def test_a_figure_stated_twice_is_one_figure(self):
        assert slot("Absolut 1L (1000ml)") == "1000ml"

    def test_from_stored_and_as_quantity(self):
        assert quantity_key(from_stored(70, "cl")) == "unknown", "the stored unit must already be canonical"
        assert quantity_key(from_stored(700, "ml")) == "700ml"
        assert quantity_key(from_stored(480, "g", pack_count=24, pack_unit_value=20, form="pack")) == "480g|pack"
        assert as_quantity((70, "cl")) == Quantity(value=700, unit="ml", form="single", state="stated")
        assert as_quantity((None, None)) is None


class TestTheKey:
    def test_every_dimension_spells_its_unit_and_never_agrees_across_dimensions(self):
        assert match_key("Dior", "Sauvage Stick Deodorant 75g", 75, vertical="beauty") == "dior|sauvage-stick-deodorant||75g"
        assert match_key("Dior", "Sauvage Deodorant 75ml", 75, vertical="beauty") == "dior|sauvage-deodorant||75ml"
        assert match_key("Chivas Regal", "Chivas 12 Year 70cl", 700, vertical="liquor") == "chivas-regal|12||700ml"
        assert match_key("X", "Widget 200pcs", None, vertical="confectionery").endswith("|200pcs")

    def test_an_unknown_keys_apart_from_every_figure(self):
        assert match_key("Dior", "Sauvage EDT", None, vertical="beauty") == "dior|sauvage|edt|unknown"
        assert match_key("Dior", "Sauvage EDT", 100, vertical="beauty") == "dior|sauvage|edt|100ml"

    def test_a_quantity_passed_in_wins_over_the_name(self):
        q = from_stored(100, "ml")
        assert match_key("Dior", "Sauvage EDT 60ml", 60, vertical="beauty", quantity=q) == "dior|sauvage|edt|100ml"


class TestCeilings:
    def test_the_ceilings_moved_and_the_wrapper_still_answers(self):
        assert CEILING_ML["beauty"] == 1000
        assert size_is_implausible("beauty", "Mist", 7624)
        assert not size_is_implausible("liquor", "Royal Export 5.8% 24x0.33LDS", 7920)
        assert not size_is_implausible("liquor", "Big 5L", 5000)
