from unittest.mock import patch

from app.services.pairing.recommendations import VALID_EVENTS, decide

ROW = {"product_key": "sku:BR-42", "name": "Blue Runner", "description": "A shoe",
       "image_url": None, "product_url": "https://s.com/p/1", "category": "shoes",
       "ctas": []}

ALL_ON = {"features": {"product_recommendation": True}}


def test_the_event_list_matches_the_contract():
    assert VALID_EVENTS == {"page_view", "dwell", "search",
                            "add_to_cart", "cart_abandon", "checkout_start"}
    assert "exit_intent" not in VALID_EVENTS


def test_an_unknown_page_recommends_nothing():
    with patch("app.services.pairing.recommendations.get_tool_settings", return_value=ALL_ON), \
         patch("app.services.pairing.recommendations.find_by_url", return_value={}):
        out = decide("org_x", "dwell", {"url": "https://s.com/unknown", "dwell_seconds": 60})
    assert out == {"recommend": False, "reason": "page_unknown"}


def test_the_switch_being_off_recommends_nothing():
    with patch("app.services.pairing.recommendations.get_tool_settings",
               return_value={"features": {"product_recommendation": False}}):
        out = decide("org_x", "dwell", {"url": "https://s.com/p/1", "dwell_seconds": 60})
    assert out == {"recommend": False, "reason": "feature_off"}


def test_dwell_under_the_threshold_recommends_nothing():
    with patch("app.services.pairing.recommendations.get_tool_settings", return_value=ALL_ON), \
         patch("app.services.pairing.recommendations.find_by_url", return_value=ROW):
        out = decide("org_x", "dwell", {"url": "https://s.com/p/1", "dwell_seconds": 5})
    assert out == {"recommend": False, "reason": "no_rule_matched"}


def test_dwell_over_the_threshold_recommends_the_same_category():
    # The graph is the source of neighbours now; related_products only fires
    # when the graph has nothing for this product (see test_recommendations_graph.py).
    other = dict(ROW, product_key="sku:TR-11", name="Trail Runner",
                 product_url="https://s.com/p/2")
    with patch("app.services.pairing.recommendations.get_tool_settings", return_value=ALL_ON), \
         patch("app.services.pairing.recommendations.find_by_url", return_value=ROW), \
         patch("app.services.pairing.recommendations.servable_neighbours", return_value=[other]):
        out = decide("org_x", "dwell", {"url": "https://s.com/p/1", "dwell_seconds": 45})
    assert out["recommend"] is True
    assert out["message"]
    names = [p["name"] for p in out["products"]]
    assert "Trail Runner" in names
    assert "Blue Runner" not in names, "the page they are already on is not a recommendation"


def test_the_search_term_is_read_from_the_url():
    """The frontend sends four fields; a search page already carries its term."""
    from app.services.pairing.recommendations import search_query_from_url
    assert search_query_from_url("https://s.com/search?q=running+shoes") == "running shoes"
    assert search_query_from_url("https://s.com/search?s=boots&utm_source=g") == "boots"
    assert search_query_from_url("https://s.com/search") == ""
    assert search_query_from_url("") == ""


def test_a_search_page_with_no_term_recommends_nothing():
    with patch("app.services.pairing.recommendations.get_tool_settings", return_value=ALL_ON), \
         patch("app.services.pairing.recommendations.find_by_url", return_value=ROW):
        out = decide("org_x", "search", {"url": "https://s.com/search"})
    assert out == {"recommend": False, "reason": "no_rule_matched"}


def test_a_search_page_with_a_term_matches_on_text_not_embeddings():
    """The Critical this guards: search must never call the embedding model.

    That endpoint fires on every page view of every visitor, unauthenticated,
    and a model call there would spend the tenant's ai_tokens quota on people
    who never open the widget. If this branch is ever pointed back at
    `search_products` (which embeds the query), this test must fail.
    """
    other = dict(ROW, product_key="sku:TR-11", name="Trail Runner",
                 product_url="https://s.com/p/2")
    with patch("app.services.pairing.recommendations.get_tool_settings", return_value=ALL_ON), \
         patch("app.services.pairing.recommendations.find_by_url", return_value=ROW), \
         patch("app.services.pairing.recommendations.match_products", return_value=[other]) as mp, \
         patch("app.services.infra.embedding.get_embeddings") as embed:
        out = decide("org_x", "search", {"url": "https://s.com/search?q=trail+runner"})
    assert out["recommend"] is True
    assert [p["name"] for p in out["products"]] == ["Trail Runner"]
    mp.assert_called_once_with("org_x", "trail runner", limit=4)
    embed.assert_not_called()


def test_an_event_with_no_rule_recommends_nothing():
    with patch("app.services.pairing.recommendations.get_tool_settings", return_value=ALL_ON), \
         patch("app.services.pairing.recommendations.find_by_url", return_value=ROW):
        out = decide("org_x", "checkout_start", {"url": "https://s.com/p/1"})
    assert out == {"recommend": False, "reason": "no_rule_matched"}


def test_a_page_with_nothing_else_to_suggest_recommends_nothing():
    # Both the graph and its related_keys fallback come up empty here.
    with patch("app.services.pairing.recommendations.get_tool_settings", return_value=ALL_ON), \
         patch("app.services.pairing.recommendations.find_by_url", return_value=ROW), \
         patch("app.services.pairing.recommendations.servable_neighbours", return_value=[]), \
         patch("app.services.pairing.recommendations.related_products", return_value=[]):
        out = decide("org_x", "dwell", {"url": "https://s.com/p/1", "dwell_seconds": 45})
    assert out == {"recommend": False, "reason": "no_match"}


def test_a_failure_recommends_nothing_rather_than_raising():
    """A suggestion nobody asked for must never surface an error."""
    with patch("app.services.pairing.recommendations.get_tool_settings",
               side_effect=RuntimeError("db down")):
        out = decide("org_x", "dwell", {"url": "https://s.com/p/1", "dwell_seconds": 45})
    assert out["recommend"] is False
    assert out["reason"] == "no_match"
