from app.services.pairing.candidates import (
    MAX_CROSS_CATEGORY_PARTNERS, blocks, candidate_pairs, is_eligible,
)


def product(key, category="shirts", **kw):
    base = {"product_key": key, "name": key, "category": category,
            "in_stock": True, "missing_fields": [], "attributes": [{"key": "color"}],
            "price_cents": 1000, "is_accessory": False}
    base.update(kw)
    return base


def test_cross_category_partners_are_capped_per_anchor():
    # The cap is the cost control for large catalogs. Without it one anchor in a
    # colour-rich catalog pairs against every other product in it.
    coloured = [{"key": "color", "value": "black"}]
    anchor = product("anchor", "shirts", attributes=coloured)
    others = [product(f"acc{i}", f"cat{i}", is_accessory=True, attributes=coloured)
              for i in range(MAX_CROSS_CATEGORY_PARTNERS * 2)]

    cross = [(a, b) for a, b in candidate_pairs([anchor] + others)
             if a["product_key"] == "anchor"
             and b["category"] != a["category"]]
    assert len(cross) == MAX_CROSS_CATEGORY_PARTNERS


def test_a_normal_product_is_eligible():
    assert is_eligible(product("a")) is True


def test_an_out_of_stock_product_is_not_paired():
    # Recommending something unbuyable is worse than recommending nothing.
    assert is_eligible(product("a", in_stock=False)) is False


def test_a_flagged_product_is_not_paired():
    assert is_eligible(product("a", missing_fields=["product_url"])) is False


def test_a_product_with_no_attributes_is_not_paired():
    # Phase 1 gave every extractable product attributes. One with none has no
    # usable text either, so every score it takes part in would be noise.
    assert is_eligible(product("a", attributes=[])) is False


def test_products_group_by_leaf_category():
    grouped = blocks([product("a", "shirts"), product("b", "shirts"),
                      product("c", "belts")])
    assert set(grouped) == {"shirts", "belts"}
    assert len(grouped["shirts"]) == 2


def test_an_uncategorised_product_gets_its_own_block():
    grouped = blocks([product("a", None)])
    assert len(grouped) == 1


def test_pairs_are_ordered_both_ways():
    # complement and upsell are directional, so (a,b) and (b,a) are different
    # questions and both must be offered to the scorer.
    pairs = candidate_pairs([product("a"), product("b")])
    assert ("a", "b") in [(x["product_key"], y["product_key"]) for x, y in pairs]
    assert ("b", "a") in [(x["product_key"], y["product_key"]) for x, y in pairs]


def test_a_product_is_never_paired_with_itself():
    pairs = candidate_pairs([product("a"), product("b")])
    assert all(x["product_key"] != y["product_key"] for x, y in pairs)


def test_pair_count_is_bounded_by_blocking():
    # 40 products spread over 4 categories must cost far less than 40*39.
    many = [product(f"p{i}", f"cat{i % 4}") for i in range(40)]
    assert len(candidate_pairs(many)) < 40 * 39


def test_products_in_unrelated_blocks_are_still_paired_across():
    # Complements live across categories -- blocking must not make them
    # impossible, only cheaper.
    pairs = candidate_pairs([product("a", "phones"),
                             product("b", "cases", is_accessory=True)])
    assert pairs


def test_two_non_accessories_with_colours_are_paired_across_categories():
    # The outfit case: a shirt and trousers complement each other and neither
    # is an accessory. Without this the weak-complement rule is unreachable and
    # clothing never pairs at all.
    shirt = product("shirt", "shirts",
                    attributes=[{"key": "color", "value": "white"}])
    trousers = product("trousers", "trousers",
                       attributes=[{"key": "color", "value": "navy"}])
    keys = [(a["product_key"], b["product_key"])
            for a, b in candidate_pairs([shirt, trousers])]
    assert ("shirt", "trousers") in keys


def test_colourless_products_are_not_paired_across_categories():
    # Nothing to score them on, so the pair would be pure cost.
    a = product("phone", "phones", attributes=[{"key": "material", "value": "glass"}])
    b = product("bread", "groceries", attributes=[{"key": "material", "value": "flour"}])
    assert candidate_pairs([a, b]) == []


def test_a_product_with_no_reference_price_is_still_paired():
    # Currency is optional on an HTTP source, so a catalogue can legitimately
    # have no converted price. Treating that as fatal made currency mandatory
    # in effect: the whole catalogue ingested and paired nothing.
    assert is_eligible(product("a", missing_fields=["price_reference"])) is True


def test_a_product_with_no_url_is_still_not_paired():
    assert is_eligible(product("a", missing_fields=["product_url"])) is False


def test_a_blocking_gap_wins_over_a_harmless_one():
    assert is_eligible(product(
        "a", missing_fields=["price_reference", "product_url"])) is False
