import pytest

import app.services.pairing.recommendations as mod

TENANT = "org_test"
PAGE = {"url": "https://shop.example/p/anchor", "dwell_seconds": 60}

ANCHOR = {"product_key": "a", "name": "Anchor", "product_url": PAGE["url"],
          "ctas": [], "options": []}


def _neighbour(key, pair_type="similar", score=0.9):
    return {"product_key": key, "name": key.title(), "ctas": [], "options": [],
            "product_url": f"https://shop.example/p/{key}",
            "pair_type": pair_type, "pair_score": score}


@pytest.fixture(autouse=True)
def wiring(monkeypatch):
    monkeypatch.setattr(mod, "get_tool_settings",
                        lambda t: {"features": {"product_recommendation": True}})
    monkeypatch.setattr(mod, "find_by_url", lambda t, u: dict(ANCHOR))
    monkeypatch.setattr(mod, "related_products", lambda t, row, limit=3: [])
    monkeypatch.setattr(mod, "servable_neighbours",
                        lambda t, key, limit=3: [])


def test_graph_neighbours_are_served(monkeypatch):
    monkeypatch.setattr(mod, "servable_neighbours",
                        lambda t, key, limit=3: [_neighbour("b"), _neighbour("c")])

    result = mod.decide(TENANT, "dwell", PAGE)
    assert result["recommend"] is True
    assert [p["product_id"] for p in result["products"]] == ["b", "c"]


def test_every_card_carries_its_attribution(monkeypatch):
    # Phase 5 cannot measure which pair types earn their place without this.
    monkeypatch.setattr(mod, "servable_neighbours",
                        lambda t, key, limit=3: [_neighbour("b", "complement", 0.77)])

    card = mod.decide(TENANT, "dwell", PAGE)["products"][0]
    assert card["pair_type"] == "complement"
    assert card["pair_score"] == pytest.approx(0.77)


def test_the_anchor_is_never_recommended_to_itself(monkeypatch):
    monkeypatch.setattr(mod, "servable_neighbours",
                        lambda t, key, limit=3: [_neighbour("a"), _neighbour("b")])
    keys = [p["product_id"] for p in mod.decide(TENANT, "dwell", PAGE)["products"]]
    assert "a" not in keys


def test_at_most_three_cards(monkeypatch):
    monkeypatch.setattr(mod, "servable_neighbours",
                        lambda t, key, limit=3: [_neighbour(k)
                                                 for k in "bcdef"])
    assert len(mod.decide(TENANT, "dwell", PAGE)["products"]) <= 3


def test_an_empty_graph_falls_back_to_related_keys(monkeypatch):
    # A tenant who has never run pairing must be no worse off than today.
    monkeypatch.setattr(mod, "servable_neighbours", lambda t, key, limit=3: [])
    monkeypatch.setattr(mod, "related_products",
                        lambda t, row, limit=3: [_neighbour("legacy")])

    keys = [p["product_id"] for p in mod.decide(TENANT, "dwell", PAGE)["products"]]
    assert keys == ["legacy"]


def test_the_graph_wins_when_both_exist(monkeypatch):
    monkeypatch.setattr(mod, "servable_neighbours",
                        lambda t, key, limit=3: [_neighbour("graph")])
    monkeypatch.setattr(mod, "related_products",
                        lambda t, row, limit=3: [_neighbour("legacy")])

    keys = [p["product_id"] for p in mod.decide(TENANT, "dwell", PAGE)["products"]]
    assert keys == ["graph"]


def test_neither_source_means_no_recommendation():
    result = mod.decide(TENANT, "dwell", PAGE)
    assert result["recommend"] is False
    assert result["reason"] == "no_match"


def test_a_failing_lookup_never_raises(monkeypatch):
    def boom(*a, **kw):
        raise RuntimeError("database down")

    monkeypatch.setattr(mod, "servable_neighbours", boom)
    result = mod.decide(TENANT, "dwell", PAGE)
    assert result["recommend"] is False


def test_short_dwell_still_recommends_nothing(monkeypatch):
    monkeypatch.setattr(mod, "servable_neighbours",
                        lambda t, key, limit=3: [_neighbour("b")])
    result = mod.decide(TENANT, "dwell",
                        {"url": PAGE["url"], "dwell_seconds": 5})
    assert result["recommend"] is False


def test_the_feature_flag_still_wins(monkeypatch):
    monkeypatch.setattr(mod, "get_tool_settings",
                        lambda t: {"features": {"product_recommendation": False}})
    monkeypatch.setattr(mod, "servable_neighbours",
                        lambda t, key, limit=3: [_neighbour("b")])
    assert mod.decide(TENANT, "dwell", PAGE)["reason"] == "feature_off"


def test_the_search_branch_still_uses_text_matching(monkeypatch):
    # Not the embedding search: that spends ai_tokens on an unauthenticated
    # endpoint firing on every page view.
    called = {}

    def _match(t, q, limit=4):
        called["q"] = q
        return [_neighbour("hit")]

    monkeypatch.setattr(mod, "match_products", _match)
    monkeypatch.setattr(mod, "servable_neighbours",
                        lambda t, key, limit=3: [_neighbour("graph")])

    result = mod.decide(TENANT, "search",
                        {"url": "https://shop.example/search?q=boots"})
    assert called["q"] == "boots"
    assert [p["product_id"] for p in result["products"]] == ["hit"]


def test_nothing_reachable_from_decide_calls_a_model(monkeypatch):
    # The hard constraint of this whole path: it runs on every page view of
    # every visitor on an unauthenticated endpoint.
    import app.core.llm_client as llm

    def forbidden(*a, **kw):
        raise AssertionError("decide() must never call a model")

    monkeypatch.setattr(llm.client.chat.completions, "create", forbidden,
                        raising=False)
    monkeypatch.setattr(llm.client.embeddings, "create", forbidden,
                        raising=False)
    monkeypatch.setattr(mod, "servable_neighbours",
                        lambda t, key, limit=3: [_neighbour("b")])

    assert mod.decide(TENANT, "dwell", PAGE)["recommend"] is True
