import pytest
from psycopg2.extras import Json, RealDictCursor

import app.services.pairing.job as pairing
from app.services.infra.database import bootstrap_tenant, get_db_connection
from app.services.pairing.decisions import is_servable, load as load_decisions, record


def _insert(schema, key, name, category, price, accessory=False, rating=4.0,
            in_stock=True, color="black", content_hash=None):
    conn = get_db_connection()
    try:
        with conn.cursor() as cur:
            cur.execute(
                f'INSERT INTO "{schema}".strategist_products '
                "(product_key, name, product_url, category, price_cents, "
                " is_accessory, rating, in_stock, attributes, content_hash) "
                "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)",
                (key, name, f"https://x.example/{key}", category, price,
                 accessory, rating, in_stock,
                 Json([{"key": "color", "value": color}]), content_hash))
        conn.commit()
    finally:
        conn.close()


def _set_in_stock(schema, key, in_stock):
    conn = get_db_connection()
    try:
        with conn.cursor() as cur:
            cur.execute(
                f'UPDATE "{schema}".strategist_products '
                "SET in_stock = %s WHERE product_key = %s", (in_stock, key))
        conn.commit()
    finally:
        conn.close()


def _neighbors(schema):
    conn = get_db_connection()
    try:
        with conn.cursor(cursor_factory=RealDictCursor) as cur:
            cur.execute(
                f'SELECT anchor_key, neighbor_key, pair_type, score, '
                f'confidence, source FROM "{schema}".strategist_product_neighbors')
            return [dict(r) for r in cur.fetchall()]
    finally:
        conn.close()


@pytest.fixture
def catalog(temp_tenant):
    # Two phones (a cheap one and a pricier one, for the upsell) plus a case
    # marked as an accessory (for the complement) -- the smallest catalogue
    # that exercises similar, upsell and complement in one run.
    bootstrap_tenant(temp_tenant)
    _insert(temp_tenant, "phone:1", "Cheap Phone", "phones", 50000)
    _insert(temp_tenant, "phone:2", "Pricey Phone", "phones", 90000, rating=4.2)
    _insert(temp_tenant, "case:1", "Phone Case", "cases", 2000, accessory=True)
    return temp_tenant


@pytest.fixture
def fixed_vectors(monkeypatch):
    # Every product maps to the same vector, so cosine similarity is always
    # 1.0 and scoring is deterministic without a real embedding call.
    def fake(tenant_id, products):
        return {p["product_key"]: [1.0, 0.0] for p in products}

    monkeypatch.setattr(pairing, "vectors_for", fake)


def test_a_rejected_pair_does_not_return_after_a_re_run(catalog, fixed_vectors):
    # THE most important test in this phase. Re-running must never overrule a
    # human. If this passes for the wrong reason the whole approval flow is a
    # lie, so the assertion reads the decisions table back rather than
    # trusting pair_tenant's own report.
    pairing.pair_tenant(catalog)

    record(catalog, [{"anchor_key": "phone:1", "neighbor_key": "phone:2",
                      "pair_type": "upsell", "decision": "rejected",
                      "decided_by": "merchant@example.com"}])

    pairing.pair_tenant(catalog)

    decisions = load_decisions(catalog)
    assert decisions[("phone:1", "phone:2", "upsell")] == "rejected"

    neighbors = _neighbors(catalog)
    upsell = next(n for n in neighbors if n["anchor_key"] == "phone:1"
                  and n["neighbor_key"] == "phone:2" and n["pair_type"] == "upsell")
    assert is_servable(upsell, decisions[("phone:1", "phone:2", "upsell")]) is False


def test_a_re_run_does_not_duplicate_rows(catalog, fixed_vectors):
    pairing.pair_tenant(catalog)
    first_count = len(_neighbors(catalog))

    pairing.pair_tenant(catalog)
    second_count = len(_neighbors(catalog))

    assert first_count > 0
    assert first_count == second_count


def test_a_pair_that_stops_scoring_disappears(catalog, fixed_vectors):
    pairing.pair_tenant(catalog)
    before = _neighbors(catalog)
    assert any(n["anchor_key"] == "phone:2" or n["neighbor_key"] == "phone:2"
              for n in before)

    _set_in_stock(catalog, "phone:2", False)
    pairing.pair_tenant(catalog)

    after = _neighbors(catalog)
    assert not any(n["anchor_key"] == "phone:2" or n["neighbor_key"] == "phone:2"
                  for n in after)


def test_a_phone_gets_a_case_and_the_case_does_not_get_a_phone(catalog, fixed_vectors):
    pairing.pair_tenant(catalog)
    neighbors = _neighbors(catalog)

    assert any(n["anchor_key"] == "phone:1" and n["neighbor_key"] == "case:1"
              and n["pair_type"] == "complement" for n in neighbors)
    assert not any(n["anchor_key"] == "case:1" and n["neighbor_key"] == "phone:1"
                  and n["pair_type"] == "complement" for n in neighbors)


def test_an_out_of_stock_product_is_never_paired(catalog, fixed_vectors):
    _insert(catalog, "phone:3", "Dead Stock Phone", "phones", 60000,
            in_stock=False)

    pairing.pair_tenant(catalog)

    neighbors = _neighbors(catalog)
    assert not any(n["anchor_key"] == "phone:3" or n["neighbor_key"] == "phone:3"
                  for n in neighbors)
