import json
import pathlib

import pytest

from app.services.catalog.fieldmap_proposer import (
    NOT_MODELLED, TARGET_FIELDS, infer_url_template, propose_field_map, summarise_record,
    validate_proposal,
)

SAMPLE = json.loads(
    pathlib.Path("tests/fixtures/http/dummyjson-page.json").read_text()
)["products"][0]


def test_summary_keeps_shape_and_drops_bulk():
    long_text = "x" * 5000
    summary = summarise_record({"id": 1, "title": "T", "description": long_text,
                                "tags": ["a", "b"], "dims": {"w": 1}})
    assert summary["id"]["type"] == "int"
    assert summary["tags"]["type"] == "list"
    assert summary["dims"]["type"] == "dict"
    # A single record can be tens of kilobytes of prose that teaches the model
    # nothing about shape.
    assert len(summary["description"]["example"]) <= 80


def test_validate_drops_paths_that_do_not_resolve():
    kept, dropped = validate_proposal(
        {"title": "$.title", "brand": "$.nope", "price": "$.price"}, SAMPLE)
    assert kept == {"title": "$.title", "price": "$.price"}
    assert dropped == ["brand"]


def test_validate_drops_malformed_paths_without_raising():
    kept, dropped = validate_proposal({"title": "title", "price": "$.price"}, SAMPLE)
    assert kept == {"price": "$.price"}
    assert "title" in dropped


def test_validate_drops_unknown_target_names():
    kept, dropped = validate_proposal(
        {"title": "$.title", "not_a_target": "$.id"}, SAMPLE)
    assert "not_a_target" not in kept


def test_url_template_and_currency_are_never_asked_of_the_model():
    # Neither appears in a payload that does not contain them, so asking the
    # model invites a hallucinated path. They are inferred separately.
    for name in NOT_MODELLED:
        assert name not in TARGET_FIELDS


@pytest.mark.parametrize("base_url,expected", [
    ("https://dummyjson.com/products",
     "https://dummyjson.com/products/{external_id}"),
    ("https://dummyjson.com/products?limit=30",
     "https://dummyjson.com/products/{external_id}"),
    ("https://shop.example.com/api/v2/items/",
     "https://shop.example.com/api/v2/items/{external_id}"),
])
def test_url_template_inferred_from_the_base_url(base_url, expected):
    # A REST collection endpoint almost always addresses one member by id.
    assert infer_url_template(base_url, {"external_id": "$.id"}) == expected


def test_no_url_template_without_an_id_mapping():
    # Without an id there is nothing to substitute, so guessing would produce a
    # template that renders a broken link for every product.
    assert infer_url_template("https://dummyjson.com/products", {}) is None


def test_propose_returns_only_fields(monkeypatch):
    import app.services.catalog.fieldmap_proposer as mod
    monkeypatch.setattr(mod, "_call_llm",
                        lambda summary: {"title": "$.title", "price": "$.price"})

    out = propose_field_map(SAMPLE)
    assert out["fields"] == {"title": "$.title", "price": "$.price"}


def test_propose_survives_an_llm_failure(monkeypatch):
    # A model outage must leave the source connected and retryable, not broken.
    import app.services.catalog.fieldmap_proposer as mod

    def boom(summary):
        raise RuntimeError("model unavailable")

    monkeypatch.setattr(mod, "_call_llm", boom)
    assert propose_field_map(SAMPLE)["fields"] == {}


def test_propose_survives_non_json_from_the_model(monkeypatch):
    import app.services.catalog.fieldmap_proposer as mod
    monkeypatch.setattr(mod, "_call_llm", lambda summary: "not a dict")
    assert propose_field_map(SAMPLE)["fields"] == {}


def test_a_path_resolving_to_the_wrong_type_is_dropped():
    # The case from the live run: "$.reviews" resolves to a list of review
    # objects, not a count, and would reach an INT column.
    sample = {"id": 1, "title": "T", "reviews": [{"rating": 5}, {"rating": 4}]}
    kept, dropped = validate_proposal(
        {"title": "$.title", "review_count": "$.reviews"}, sample)
    assert kept == {"title": "$.title"}
    assert "review_count" in dropped


def test_a_correctly_typed_path_is_kept():
    sample = {"id": 1, "reviewCount": 120, "tags": ["a"]}
    kept, _ = validate_proposal(
        {"review_count": "$.reviewCount", "tags": "$.tags"}, sample)
    assert kept == {"review_count": "$.reviewCount", "tags": "$.tags"}


def test_a_boolean_is_not_accepted_as_a_number():
    sample = {"rating": True}
    kept, dropped = validate_proposal({"rating": "$.rating"}, sample)
    assert "rating" in dropped


def test_price_as_a_string_is_accepted():
    # Several real APIs return prices as strings; to_cents handles it.
    sample = {"price": "9.99"}
    kept, _ = validate_proposal({"price": "$.price"}, sample)
    assert kept == {"price": "$.price"}
