"""Refusals are stored, not logged, and counted under their reason.

Migration #2 (Stream Q). Before it, `record_rejection` logged and bumped one
counter for every reason, so "why did 40 listings not publish" needed the
log, and the log rotated. `rejected_observations` is written by a raw INSERT
in ingest (so a container older than the table cannot die on it); these
tests keep that INSERT and the table's model from drifting apart.
"""

import inspect
import re

from app.models import CollectionRun, RejectedObservation
from app.services import ingest


class TestSkipCounts:
    def test_every_skip_is_in_the_total_and_under_its_reason(self):
        run = CollectionRun(skipped_no_price=0, skip_counts={})
        ingest.count_skip(run, "no_price")
        ingest.count_skip(run, "no_price")
        ingest.count_skip(run, "gtin_size_veto")
        assert run.skipped_no_price == 3
        assert run.skip_counts == {"no_price": 2, "gtin_size_veto": 1}

    def test_a_run_from_before_the_column_counts_too(self):
        """A row created before migration #2 loads skip_counts as None."""
        run = CollectionRun(skipped_no_price=5, skip_counts=None)
        ingest.count_skip(run, "price_quarantined")
        assert run.skipped_no_price == 6 and run.skip_counts == {"price_quarantined": 1}

    def test_the_counter_is_reassigned_not_mutated(self):
        """SQLAlchemy only notices a JSONB change when the attribute is
        reassigned; an in-place dict update would never reach the database."""
        run = CollectionRun(skipped_no_price=0, skip_counts={})
        before = run.skip_counts
        ingest.count_skip(run, "no_price")
        assert run.skip_counts is not before


class TestRejectionInsertMatchesTheTable:
    def test_the_raw_insert_names_only_columns_the_model_has(self):
        source = inspect.getsource(ingest.record_rejection)
        match = re.search(r"INSERT INTO rejected_observations \(([^)]*)\)", source)
        assert match, "record_rejection no longer inserts into rejected_observations"
        named = {c.strip() for c in match.group(1).split(",")}
        columns = set(RejectedObservation.__table__.columns.keys())
        assert named <= columns, named - columns
        required = {
            c.name for c in RejectedObservation.__table__.columns
            if not c.nullable and c.server_default is None and not c.primary_key
        }
        assert required <= named, required - named
