"""verify re-reads a published listing through the same parser and compares
what the page says now with what we show; these cases are the two records
the brief names -- the ATH Lancôme row whose name the page spells
differently, and the Glenfiddich read as 7 litres from "0.70cl" -- plus the
incidents behind each verdict.
"""

from datetime import UTC, datetime

from app.services.audit import LatestRow
from app.services.collectors.base import RawListing
from app.services.verify import (
    Verdict,
    channel_prices,
    compare,
    cross_shop_medians,
    name_overlap,
    plan_sample,
    summarise,
    targets_from_rows,
)


def published(**overrides) -> LatestRow:
    """A LatestRow for the ATH Lancôme listing, with every field defaulted."""
    base = dict(
        listing_id=1,
        product_id=1,
        location_id=1,
        location_code="ATH",
        location_currency="EUR",
        visible=True,
        source_slug="avolta-ath",
        price=112.83,
        currency="EUR",
        price_usd=112.83,
        was_price=None,
        in_stock=True,
        observed_at=datetime(2026, 9, 5, tzinfo=UTC),
        fx_source="live",
        url="https://athens.shopdutyfree.com/en/x",
        source_sku="5074177",
        name="Lancome La Vie Est Belle 50ml",
        brand="Lancome",
        gtin=None,
        size_ml=50,
        vertical="beauty",
        category="fragrance",
    )
    base.update(overrides)
    return LatestRow(**base)


def live(**overrides) -> RawListing:
    """A RawListing echoing `published()`'s defaults, as read back from the page."""
    base = dict(
        source_sku="5074177",
        name="Lancome La Vie Est Belle 50ml",
        price=112.83,
        currency="EUR",
        location_code="ATH",
        brand="Lancome",
        gtin=None,
        size_ml=50,
        in_stock=True,
        url="https://athens.shopdutyfree.com/en/x",
    )
    base.update(overrides)
    return RawListing(**base)


class TestNameOverlap:
    def test_the_ath_lancome_row_sits_exactly_at_the_line(self):
        """"Lancome La Vie Est Belle 50ml" vs the page's "La Vie Est Belle Eau
        de Parfum 50ml": 3 words shared of the shorter side's 4."""
        overlap = name_overlap("Lancome La Vie Est Belle 50ml", "La Vie Est Belle Eau de Parfum 50ml")
        assert overlap == 0.75

    def test_identical_names_overlap_completely(self):
        assert name_overlap("Glenfiddich 15 Sherry Cask", "Glenfiddich 15 Sherry Cask") == 1.0

    def test_two_unrelated_bottles_overlap_below_the_line(self):
        assert name_overlap("Glenfiddich 15 Sherry Cask", "Bombay Sapphire Gin 1L") < 0.75

    def test_an_empty_name_has_nothing_to_overlap(self):
        assert name_overlap("", "x") == 0.0


class TestChannelPrices:
    def test_a_single_size_page_carries_both_channels_agreeing(self):
        raw = {"jsonld": {"offers": [{"price": "45.00"}]}, "variation": {"duty_free": {"price": 45}}}
        assert channel_prices(raw) == (45.0, 45.0)

    def test_an_offers_object_rather_than_a_list_is_still_read(self):
        raw = {"jsonld": {"offers": {"price": "45.00"}}}
        assert channel_prices(raw) == (45.0, None)

    def test_a_familys_starting_price_is_not_a_channel(self):
        """lowPrice is the family's cheapest size, not this page's price."""
        raw = {"jsonld": {"offers": [{"lowPrice": "45.00"}]}}
        assert channel_prices(raw) == (None, None)

    def test_no_raw_fragment_reads_as_no_channels(self):
        assert channel_prices(None) == (None, None)

    def test_an_unparseable_price_reads_as_no_channel(self):
        raw = {"jsonld": {"offers": [{"price": "n/a"}]}}
        assert channel_prices(raw) == (None, None)


class TestCompare:
    def test_a_listing_the_page_no_longer_offers_is_gone(self):
        assert compare(published(), None, median_usd=None, rate=0.86).verdict == "GONE"

    def test_a_zero_price_does_not_parse(self):
        assert compare(published(), live(price=0.0), median_usd=None, rate=0.86).verdict == "PARSE_FAIL"

    def test_a_missing_price_does_not_parse(self):
        assert compare(published(), live(price=None), median_usd=None, rate=0.86).verdict == "PARSE_FAIL"

    def test_the_mex_store_rendered_us_and_mx_on_one_line(self):
        """A currency mismatch is checked before identity or price."""
        verdict = compare(published(), live(currency="USD"), median_usd=None, rate=0.86)
        assert verdict.verdict == "MISMATCH_CURRENCY"

    def test_two_different_barcodes_are_a_mismatched_identity(self):
        pub = published(gtin="5010327000206")
        verdict = compare(pub, live(gtin="5010327000213"), median_usd=None, rate=0.86)
        assert verdict.verdict == "MISMATCH_IDENTITY"
        assert "gtin" in verdict.detail

    def test_the_0_70cl_glenfiddich_misread_as_7_litres(self):
        """The record behind the 7,000 ml Glenfiddich: two 7-litre products
        were published with prices from a "0.70cl" misread."""
        pub = published(name="Glenfiddich 15 Sherry Cask", brand="Glenfiddich", size_ml=700, price=75.0)
        verdict = compare(pub, live(name="Glenfiddich 15 Sherry Cask", size_ml=7000, price=75.0),
                          median_usd=None, rate=0.86)
        assert verdict.verdict == "MISMATCH_SIZE"

    def test_700_vs_750_ml_is_feed_sloppiness_not_a_mismatch(self):
        pub = published(size_ml=700)
        verdict = compare(pub, live(size_ml=750), median_usd=None, rate=0.86)
        assert verdict.verdict == "PASS"

    def test_the_ath_lancome_spelling_passes_at_the_line(self):
        verdict = compare(published(), live(name="La Vie Est Belle Eau de Parfum 50ml"),
                          median_usd=None, rate=0.86)
        assert verdict.verdict == "PASS"

    def test_a_name_below_the_overlap_line_is_a_mismatched_identity(self):
        verdict = compare(published(), live(name="Bombay Sapphire Gin 1L"), median_usd=None, rate=0.86)
        assert verdict.verdict == "MISMATCH_IDENTITY"
        assert "name overlap" in verdict.detail

    def test_a_shared_barcode_makes_the_name_just_a_spelling(self):
        pub = published(gtin="5010327000206")
        verdict = compare(pub, live(gtin="5010327000206", name="Totally Different Name"),
                          median_usd=None, rate=0.86)
        assert verdict.verdict == "PASS"

    def test_a_five_percent_move_is_recorded(self):
        verdict = compare(published(), live(price=118.47), median_usd=None, rate=0.86)
        assert verdict.verdict == "PRICE_MOVED"
        assert "112.83 -> 118.47" in verdict.detail

    def test_the_same_price_passes_with_no_detail(self):
        verdict = compare(published(), live(price=112.83), median_usd=None, rate=0.86)
        assert verdict.verdict == "PASS"
        assert verdict.detail == ""

    def test_a_sub_half_cent_rounding_is_not_a_move(self):
        verdict = compare(published(), live(price=112.834), median_usd=None, rate=0.86)
        assert verdict.verdict == "PASS"

    def test_a_77_percent_move_needs_a_human(self):
        verdict = compare(published(), live(price=200.0), median_usd=None, rate=0.86)
        assert verdict.verdict == "REVIEW"
        assert "moved" in verdict.detail

    def test_a_price_far_below_the_cross_shop_median_needs_a_human(self):
        pub = published(price=40.0, price_usd=46.51)
        verdict = compare(pub, live(price=40.0), median_usd=200.0, rate=0.86)
        assert verdict.verdict == "REVIEW"
        assert "median" in verdict.detail

    def test_the_same_price_inside_the_median_band_passes(self):
        pub = published(price=40.0, price_usd=46.51)
        verdict = compare(pub, live(price=40.0), median_usd=50.0, rate=0.86)
        assert verdict.verdict == "PASS"

    def test_no_fx_rate_means_the_median_band_is_not_applied(self):
        pub = published(price=40.0, price_usd=46.51)
        verdict = compare(pub, live(price=40.0), median_usd=200.0, rate=None)
        assert verdict.verdict == "PASS"

    def test_json_ld_and_the_rsc_payload_disagreeing_needs_a_human(self):
        raw = {"jsonld": {"offers": [{"price": "45.00"}]}, "variation": {"duty_free": {"price": 50}}}
        verdict = compare(published(), live(raw=raw, price=112.83), median_usd=None, rate=0.86)
        assert verdict.verdict == "REVIEW"
        assert "channels disagree" in verdict.detail

    def test_the_two_channels_agreeing_is_not_a_finding(self):
        raw = {"jsonld": {"offers": [{"price": "112.83"}]}, "variation": {"duty_free": {"price": 112.83}}}
        verdict = compare(published(), live(raw=raw, price=112.83), median_usd=None, rate=0.86)
        assert verdict.verdict == "PASS"

    def test_a_move_and_a_channel_disagreement_both_show_in_one_review(self):
        raw = {"jsonld": {"offers": [{"price": "45.00"}]}, "variation": {"duty_free": {"price": 50}}}
        verdict = compare(published(), live(raw=raw, price=118.47), median_usd=None, rate=0.86)
        assert verdict.verdict == "REVIEW"
        assert "moved" in verdict.detail
        assert "channels" in verdict.detail

    def test_is_failure_marks_only_the_correctness_verdicts(self):
        assert Verdict("MISMATCH_SIZE").is_failure is True
        assert Verdict("PRICE_MOVED").is_failure is False
        assert Verdict("BLOCKED").is_failure is False


class TestPlanSample:
    def test_the_targeted_set_is_kept_whole_and_filled_up_to_n(self):
        """99 is targeted but is not a candidate, so it never appears."""
        plan = plan_sample(list(range(1, 11)), {3: "featured", 5: "moved", 99: "featured"}, n=5, seed=1)
        assert set(plan.targeted) == {3, 5}
        assert len(plan.filled) == 3
        assert set(plan.filled).isdisjoint(plan.targeted)
        assert all(lid in range(1, 11) for lid in plan.filled)
        assert plan.filled == sorted(plan.filled)

    def test_the_same_seed_draws_the_same_sample(self):
        first = plan_sample(list(range(1, 11)), {3: "featured", 5: "moved"}, n=5, seed=1)
        second = plan_sample(list(range(1, 11)), {3: "featured", 5: "moved"}, n=5, seed=1)
        assert first.listing_ids == second.listing_ids

    def test_a_different_seed_may_draw_a_different_sample_of_the_same_size(self):
        one = plan_sample(list(range(1, 11)), {3: "featured", 5: "moved"}, n=5, seed=1)
        two = plan_sample(list(range(1, 11)), {3: "featured", 5: "moved"}, n=5, seed=2)
        assert len(one.listing_ids) == len(two.listing_ids)

    def test_a_census_is_a_census(self):
        """A targeted set larger than n is read in full; nothing is filled."""
        candidates = list(range(1, 8))
        targeted = {lid: "featured" for lid in candidates}
        plan = plan_sample(candidates, targeted, n=5, seed=1)
        assert plan.filled == []
        assert len(plan.listing_ids) == 7

    def test_n_larger_than_the_candidates_reads_everything_once(self):
        plan = plan_sample([1, 2, 3], {}, n=10, seed=1)
        assert sorted(plan.listing_ids) == [1, 2, 3]
        assert len(plan.listing_ids) == len(set(plan.listing_ids))

    def test_reason_names_why_a_listing_is_in_the_sample(self):
        plan = plan_sample(list(range(1, 11)), {3: "featured", 5: "moved"}, n=5, seed=1)
        assert plan.reason(3) == "featured"
        filled_id = next(iter(plan.filled))
        assert plan.reason(filled_id) == "random"


class TestTargetsFromRows:
    def test_a_products_two_shops_are_both_targeted_by_saving(self):
        """Either label is acceptable: the featured pick and the top-saving
        pick both land on the same product when it is the only one."""
        rows = [
            published(listing_id=1, product_id=1, location_id=1, location_code="ATH", price_usd=20.0),
            published(listing_id=2, product_id=1, location_id=2, location_code="JFK", price_usd=44.0),
        ]
        targeted = targets_from_rows(rows, {})
        assert 1 in targeted
        assert 2 in targeted

    def test_a_product_at_one_shop_only_is_not_targeted(self):
        rows = [published(listing_id=1, product_id=1, location_id=1, price_usd=50.0)]
        assert targets_from_rows(rows, {}) == {}

    def test_a_big_mover_is_targeted(self):
        rows = [published(listing_id=7, product_id=7, location_id=1, price=100.0, price_usd=100.0)]
        targeted = targets_from_rows(rows, {7: 70.0})
        assert targeted[7] == "moved"

    def test_an_eleven_percent_move_is_under_the_target_line(self):
        rows = [published(listing_id=7, product_id=7, location_id=1, price=100.0, price_usd=100.0)]
        assert targets_from_rows(rows, {7: 90.0}) == {}

    def test_at_most_twenty_products_savings_are_targeted(self):
        rows = []
        for i in range(22):
            rows.append(published(listing_id=i * 2 + 1, product_id=i + 1, location_id=1,
                                   location_code="ATH", price=10.0 + i, price_usd=10.0 + i))
            rows.append(published(listing_id=i * 2 + 2, product_id=i + 1, location_id=2,
                                   location_code="JFK", price=50.0 + i * 3, price_usd=50.0 + i * 3))
        targeted = targets_from_rows(rows, {})
        top_saving = [reason for reason in targeted.values() if reason.startswith("top_saving")]
        assert len(top_saving) <= 40


class TestCrossShopMedians:
    def test_the_median_of_three_shops(self):
        rows = [
            published(listing_id=1, product_id=1, location_id=1, price_usd=10.0),
            published(listing_id=2, product_id=1, location_id=2, price_usd=20.0),
            published(listing_id=3, product_id=1, location_id=3, price_usd=30.0),
        ]
        assert cross_shop_medians(rows) == {1: 20.0}

    def test_a_product_at_one_shop_has_no_median(self):
        rows = [published(listing_id=1, product_id=1, location_id=1, price_usd=50.0)]
        assert cross_shop_medians(rows) == {}

    def test_a_shops_own_cheaper_row_is_the_one_that_counts(self):
        rows = [
            published(listing_id=1, product_id=1, location_id=1, price_usd=15.0),
            published(listing_id=2, product_id=1, location_id=1, price_usd=12.0),
            published(listing_id=3, product_id=1, location_id=2, price_usd=30.0),
        ]
        assert cross_shop_medians(rows) == {1: 21.0}

    def test_a_row_with_no_usd_price_is_ignored(self):
        rows = [
            published(listing_id=1, product_id=1, location_id=1, price_usd=None),
            published(listing_id=2, product_id=1, location_id=2, price_usd=20.0),
        ]
        assert cross_shop_medians(rows) == {}


class TestSummarise:
    def test_one_of_each_kind(self):
        checks = [Verdict("PASS"), Verdict("PRICE_MOVED"), Verdict("MISMATCH_SIZE"), Verdict("BLOCKED")]
        summary = summarise(checks)
        assert summary["checked"] == 4
        assert summary["failures"] == 1
        assert summary["blocked"] is True
        assert summary["PASS"] == 1

    def test_no_checks_is_not_a_failure_or_a_block(self):
        summary = summarise([])
        assert summary["checked"] == 0
        assert summary["failures"] == 0
        assert summary["blocked"] is False
