"""The suggestion rules, each pinned by the case that asked for it (Stream M, task M5).

Pure scorers over facts a session would load: no database. What is pinned is that Paco
Rabanne against Rabanne leads the brand queue with a reason a person can read, that the
initials and containment rules need shared line names, that a line pair stays inside one
brand, and that a bare "1 Million 10cl" is offered against the Eau de Toilette at its size
and never against another size or a set.
"""

from app.services.suggest import (
    BrandFacts, LineFacts, ProductFacts, brand_pairs, line_pairs, product_reason, unknown_attribute_pairs,
)


def brand(id, slug, name, lines, product_variants=10, airports=2):
    return BrandFacts(id=id, slug=slug, name=name, lines=frozenset(lines), product_variants=product_variants, airports=airports)


class TestBrandPairs:
    def test_the_rebrand_leads_and_reads_as_one_line(self):
        facts = [brand(1, "paco-rabanne", "Paco Rabanne", {"1 million", "invictus", "fame"}, 203),
                 brand(2, "rabanne", "Rabanne", {"1 million", "invictus", "phantom"}, 90),
                 brand(3, "dior", "Dior", {"sauvage"})]
        out = sorted(brand_pairs(facts), key=lambda s: -s.score)
        assert [(s.left_id, s.right_id, s.reason) for s in out] == [(1, 2, "known_rebrand")]
        assert out[0].why == "Paco Rabanne renamed itself Rabanne in 2023; the two rows are one brand"
        assert out[0].detail["shared_lines"] == ["1 million", "invictus"] and out[0].detail["shared_count"] == 2

    def test_a_name_within_another_needs_shared_lines(self):
        chivas = [brand(1, "chivas", "Chivas", {"12", "18"}), brand(2, "chivas-regal", "Chivas Regal", {"12", "18", "25"})]
        (only,) = brand_pairs(chivas)
        assert only.reason == "name_within" and only.score > 0.9
        assert only.why == '"Chivas" is within "Chivas Regal" and they share 2 line name(s)'
        assert brand_pairs([brand(1, "sun", "Sun", {"a"}), brand(2, "sun-set", "Sun Set", {"b"})]) == []

    def test_initials_and_spacing(self):
        facts = [brand(1, "calvin-klein", "Calvin Klein", {"one", "eternity men"}), brand(2, "ck", "CK", {"one"}),
                 brand(3, "glen-moray", "Glen Moray", {"12"}), brand(4, "glenmoray", "Glenmoray", {"15"}),
                 brand(5, "glenmorangie", "Glenmorangie", {"10"})]
        out = {(s.left_id, s.right_id): s.reason for s in brand_pairs(facts)}
        assert out == {(1, 2): "initials", (3, 4): "same_letters"}


class TestLinePairs:
    def line(self, id, brand, key, name, product_variants=2, sizes=(100,), vertical="beauty", category=""):
        return LineFacts(id=id, brand_id=brand, key=key, name=name, product_variants=product_variants, airports=1,
                         sizes=frozenset(sizes), vertical=vertical, category=category)

    def test_shades_and_other_families_are_not_offered(self):
        """"Foundation 230W" beside "Foundation 310N" is one product in two shades: folding the
        lines would merge the shades into one row. A foie gras is not a line to compare."""
        shades = [self.line(1, 1, "teint idole ultra wear foundation 230w", "Teint Idole Ultra Wear Foundation 230W"),
                  self.line(2, 1, "teint idole ultra wear foundation 310n", "Teint Idole Ultra Wear Foundation 310N")]
        assert line_pairs(shades) == []
        food = [LineFacts(id=1, brand_id=1, key="duck foie gras block", name="Duck Foie Gras Block", product_variants=1, airports=1,
                          sizes=frozenset({100}), vertical="confectionery"),
                LineFacts(id=2, brand_id=1, key="block of duck foie gras", name="Block Of Duck Foie Gras", product_variants=1, airports=1,
                          sizes=frozenset({100}), vertical="confectionery")]
        assert line_pairs(food) == []

    def test_two_lines_with_no_size_in_common_are_not_offered(self):
        """A merge there creates no comparison; the queue is a person's time."""
        lines = [self.line(1, 1, "diorshow mascara", "Diorshow Mascara", sizes=(10,)),
                 self.line(2, 1, "diorshow overvolume mascara", "Diorshow Overvolume Mascara", sizes=(6,))]
        assert line_pairs(lines) == []
        lines[1].sizes = frozenset({6, 10})
        assert [s.detail["shared_sizes"] for s in line_pairs(lines)] == [[10]]

    def test_within_one_house_only(self):
        lines = [self.line(1, 1, "eternity men", "Eternity For Men"),
                 self.line(2, 1, "eternity aromatic essence men", "Eternity Aromatic Essence For Men"),
                 self.line(3, 2, "eternity men", "Eternity Men")]
        out = line_pairs(lines)
        assert [(s.left_id, s.right_id, s.score, s.reason) for s in out] == [(1, 2, 0.8, "name_within")]
        assert out[0].why == 'Every word of "Eternity For Men" is in "Eternity Aromatic Essence For Men"'

    def test_the_namesake_beside_original_and_a_lone_word_scores_low(self):
        lines = [self.line(1, 1, "", "Jameson"), self.line(2, 1, "original", "Original"),
                 self.line(3, 1, "black barrel", "Black Barrel"), self.line(4, 1, "black barrel 12", "Black Barrel 12"),
                 self.line(5, 1, "12", "12"), self.line(6, 1, "12 rum cask", "12 Rum Cask")]
        out = {(s.left_id, s.right_id): (s.score, s.reason) for s in line_pairs(lines)}
        assert out[(1, 2)] == (0.7, "namesake_vs_original")
        assert (3, 4) not in out, "Black Barrel and Black Barrel 12 differ by an age: the expression, not a spelling"
        assert out[(5, 6)] == (0.5, "name_within")
        assert (1, 3) not in out and (2, 5) not in out

    def test_a_reordering_is_offered_and_a_flavour_is_not(self):
        lines = [self.line(1, 1, "7 gran reserva", "7 Gran Reserva", vertical="liquor"),
                 self.line(2, 1, "gran reserva 7", "Gran Reserva 7", vertical="liquor"),
                 self.line(3, 1, "hard iced tea peach", "Hard Iced Tea Peach", vertical="liquor"),
                 self.line(4, 1, "hard iced tea lemon", "Hard Iced Tea Lemon", vertical="liquor"),
                 self.line(5, 1, "double serum foundation l3w", "Double Serum Foundation L3W", vertical="beauty", category="Makeup"),
                 self.line(6, 1, "double serum foundation m3n", "Double Serum Foundation M3N", vertical="beauty", category="Makeup")]
        out = {(s.left_id, s.right_id): s.reason for s in line_pairs(lines)}
        assert out == {(1, 2): "words_shared"}


class TestUnknownVariation:
    def product(self, id, name, size, attribute, form="", airports=1, line=1, gtin=None):
        return ProductFacts(id=id, product_line_id=line, quantity_ml=size, attribute=attribute, form=form, name=name,
                            airports=airports, gtin=gtin)

    def test_the_bare_row_is_offered_against_each_known_variation_at_its_size(self):
        rows = [self.product(7804, "1 Million 10cl", 100, ""),
                self.product(17217, "1 Million Eau de Toilette 100 ml", 100, "edt", airports=4),
                self.product(7762, "1 Million Parfum 10cl", 100, "parfum"),
                self.product(17219, "1 Million Eau de Toilette 50 ml", 50, "edt"),
                self.product(17885, "Rabanne Set: Duo 1 Million", 100, "edt", form="set")]
        out = {(s.left_id, s.right_id): s for s in unknown_attribute_pairs(rows)}
        assert set(out) == {(7804, 17217), (7762, 7804)}
        assert out[(7804, 17217)].score == 0.4 and out[(7804, 17217)].why.startswith(
            'Same line and size; "1 Million 10cl" names no attribute and the other is Eau de Toilette, one of 2 attribute_values')

    def test_one_known_variation_scores_higher_and_a_different_barcode_is_never_offered(self):
        rows = [self.product(1, "Sauvage 100ml", 100, "", gtin="3348901250412"),
                self.product(2, "Sauvage Eau de Toilette 100ml", 100, "edt"),
                self.product(3, "Sauvage EDT 100ml", 100, "edt", gtin="3348901250429")]
        out = {(s.left_id, s.right_id): s.score for s in unknown_attribute_pairs(rows)}
        assert out == {(1, 2): 0.6}


class TestProductReason:
    def test_the_codes_read_as_a_sentence(self):
        assert product_reason("gtin_differs") == "Same brand, line, attribute and quantity, but the two rows carry different barcodes"
        assert product_reason("gtin_differs,set_vs_single").endswith("different barcodes and one is a set and the other a single item")
