"""Decision 10 on the public site: a shop whose source verify has blocked is hidden.

Verify's tripwire (one MISMATCH_* or PARSE_FAIL, uncleared) marks a source
"publication blocked", but for a day the block was visible only in
`verify-status`: every catalogue query kept showing the shop's prices as current,
so a parser reading the wrong size at one shop would have published it on every
product page and card until a human noticed. `catalog_queries.publishable(db)` is
now the one rule every query scopes its shops through, and these tests pin what it
hides, what it leaves alone, and that one request asks once.
"""

from types import SimpleNamespace

from sqlalchemy.dialects import postgresql

from app.services import catalog_queries, verify


def _sql(clause) -> str:
    return str(clause.compile(dialect=postgresql.dialect(), compile_kwargs={"literal_binds": True}))


class _Session:
    """Just enough of a session for the block lookup: `scalars` answers each of
    the three statements by what it selects, and `info` is the per-request memo."""

    def __init__(self, listing_locations=(), source_slugs=(), declared_locations=()):
        self.info: dict = {}
        self.listing_locations = list(listing_locations)
        self.source_slugs = list(source_slugs)
        self.declared_locations = list(declared_locations)
        self.asked: list[str] = []

    def scalars(self, stmt):
        text = _sql(stmt)
        self.asked.append(text)
        if "listings.location_id" in text:
            return iter(self.listing_locations)
        if "sources.slug" in text:
            return iter(self.source_slugs)
        if "locations.id" in text and "retailers" in text:
            return iter(self.declared_locations)
        raise AssertionError(f"unexpected statement: {text}")


def _check(listing_id: int):
    return SimpleNamespace(listing_id=listing_id, verdict="MISMATCH_SIZE")


class TestBlockedLocationIds:
    def test_without_a_session_nothing_is_blocked(self):
        assert catalog_queries.blocked_location_ids(None) == []

    def test_nothing_blocked_costs_one_lookup_and_no_more(self, monkeypatch):
        monkeypatch.setattr(verify, "blocked_sources", lambda db: {})
        db = _Session()
        assert catalog_queries.blocked_location_ids(db) == []
        assert db.asked == []

    def test_the_failing_checks_name_the_shop(self, monkeypatch):
        """A MISMATCH_SIZE on one Madrid listing blocks the Madrid shop (location 12)."""
        monkeypatch.setattr(verify, "blocked_sources", lambda db: {14: [_check(101), _check(102)]})
        db = _Session(listing_locations=[12, 12], source_slugs=["avolta-mad"], declared_locations=[12])
        assert catalog_queries.blocked_location_ids(db) == [12]

    def test_the_collectors_declared_shops_are_hidden_too(self, monkeypatch):
        """A platform collector serving two shops from one parser: the failing check
        landed at one of them, the block covers both."""
        monkeypatch.setattr(verify, "blocked_sources", lambda db: {8: [_check(7)]})
        db = _Session(listing_locations=[8], source_slugs=["the-loop-dublin-cork"], declared_locations=[8, 9])
        assert catalog_queries.blocked_location_ids(db) == [8, 9]

    def test_a_source_without_a_registered_collector_still_hides_its_shop(self, monkeypatch):
        monkeypatch.setattr(verify, "blocked_sources", lambda db: {99: [_check(5)]})
        db = _Session(listing_locations=[3], source_slugs=["renamed-away"], declared_locations=[])
        assert catalog_queries.blocked_location_ids(db) == [3]
        # No collector, no declared shops: the location lookup is never sent.
        assert not any("retailers" in text for text in db.asked)

    def test_one_request_asks_once(self, monkeypatch):
        calls = []

        def blocked(db):
            calls.append(1)
            return {14: [_check(101)]}

        monkeypatch.setattr(verify, "blocked_sources", blocked)
        db = _Session(listing_locations=[12], source_slugs=["avolta-mad"], declared_locations=[12])
        first = catalog_queries.blocked_location_ids(db)
        asked = len(db.asked)
        assert catalog_queries.blocked_location_ids(db) == first == [12]
        assert calls == [1] and len(db.asked) == asked


class TestPublishable:
    def test_visible_only_when_nothing_is_blocked(self, monkeypatch):
        monkeypatch.setattr(verify, "blocked_sources", lambda db: {})
        sql = _sql(catalog_queries.publishable(_Session()))
        assert "locations.visible IS true" in sql
        assert "NOT IN" not in sql

    def test_a_blocked_shop_is_excluded_by_id(self, monkeypatch):
        monkeypatch.setattr(verify, "blocked_sources", lambda db: {14: [_check(101)]})
        db = _Session(listing_locations=[12], source_slugs=["avolta-mad"], declared_locations=[12])
        sql = _sql(catalog_queries.publishable(db))
        assert "locations.visible IS true" in sql
        assert "locations.id NOT IN (12)" in sql

    def test_the_card_query_carries_the_rule(self, monkeypatch):
        """`_summary_base` feeds every list on the site (browse, airport, brand,
        featured, similar, the feed): the exclusion must be in its WHERE."""
        monkeypatch.setattr(verify, "blocked_sources", lambda db: {14: [_check(101)]})
        db = _Session(listing_locations=[12], source_slugs=["avolta-mad"], declared_locations=[12])
        sql = _sql(catalog_queries._summary_base(db))
        assert "locations.visible IS true" in sql
        assert "locations.id NOT IN (12)" in sql
        # And the same statement without a block asks only for visibility.
        monkeypatch.setattr(verify, "blocked_sources", lambda db: {})
        # Only the location clause: the category rule (taxonomy.HIDDEN_CATEGORIES)
        # adds its own NOT IN on products.category regardless of blocks.
        assert "locations.id NOT IN" not in _sql(catalog_queries._summary_base(_Session()))
