"""`app.cli featured evidence` (Stream AW2.1): the grammar before any database call, the pages
it measures, and the JSON a later side-by-side reads.

Pinned: an unknown page shape or `--first 0` exits 1 before a session opens (a typo never
touches the database); the page specs map to the site's own addresses; the default pages are
`home` plus the three largest eligible pools among the categories `category_counts` lists,
never a hard-coded three; `--json` carries every column per page; the table names each missed
target. What the wrong answer costs: a report on the wrong page, or a "before" that cannot be
put beside an "after" because the columns differ. No network, no database.
"""

from __future__ import annotations

import json
from types import SimpleNamespace

import pytest

from app import cli, cli_featured
from app.services import featured
from app.services.featured import SavingRecord


def rec(pid, usd, pct, **kw):
    return SavingRecord(pid, usd, pct, kw.pop("awarded", False), **kw)


@pytest.fixture
def no_database(monkeypatch):
    """Any attempt to open a session is the failure this pins."""
    import app.db

    def boom(*a, **k):
        raise AssertionError("a database session was opened")

    monkeypatch.setattr(app.db, "SessionLocal", boom)


class TestGrammar:
    def test_page_specs_map_to_the_site_s_addresses(self):
        assert cli_featured.parse_page("home") == ("home", None)
        assert cli_featured.parse_page("/products") == ("products", None)
        assert cli_featured.parse_page("/alcohol") == ("family", "liquor")
        assert cli_featured.parse_page("/alcohol/whisky") == ("category", "Whisky")
        assert cli_featured.parse_page("/brands/glenfiddich") == ("brand", "glenfiddich")
        assert cli_featured.parse_page("/airports/lhr") == ("airport", "lhr")

    @pytest.mark.parametrize("spec", ["/nowhere", "/alcohol/nothing", "/beauty/whisky/extra", "products", ""])
    def test_an_unknown_shape_is_refused(self, spec):
        with pytest.raises(cli_featured.PageError):
            cli_featured.parse_page(spec)

    def test_a_bad_spec_or_first_exits_1_before_any_database_call(self, no_database, capsys):
        assert cli.main(["featured", "evidence", "--pages", "/nowhere/x/y"]) == 1
        assert "unknown page" in capsys.readouterr().out
        assert cli.main(["featured", "evidence", "--first", "0"]) == 1
        assert "--first must be at least 1" in capsys.readouterr().out


class TestDefaultPages:
    def test_home_plus_the_three_largest_eligible_pools_with_a_page(self, monkeypatch):
        counts = [SimpleNamespace(category=c, count=n, family=f) for c, n, f in (
            ("Perfume", 900, "beauty"), ("Whisky", 500, "liquor"), ("Makeup", 400, "beauty"),
            ("Gin", 300, "liquor"), ("Twinpack", 9000, None),
        )]
        monkeypatch.setattr(cli_featured.catalog_queries, "category_counts", lambda db: counts)
        records = (
            [rec(i, 10, 0.2, category="Gin") for i in range(1, 8)]
            + [rec(i, 10, 0.2, category="Perfume") for i in range(10, 15)]
            + [rec(i, 10, 0.2, category="Whisky") for i in range(20, 26)]
            + [rec(i, 10, 0.2, category="Makeup") for i in range(30, 33)]
            + [rec(i, 0.0, 0.0, category="Makeup") for i in range(40, 60)]  # no saving: not eligible
        )
        pages = cli_featured.default_pages(None, records)
        assert pages == ["home", "/alcohol/gin", "/alcohol/whisky", "/beauty/perfume"]


class TestOutput:
    def loaded(self):
        pool = [rec(i, 10 + i, 0.1 + i / 100, brand_id=i, picture_tier=3, awarded=(i == 3), name=f"bottle {i}") for i in range(1, 12)]
        order = [r.variant_id for r in reversed(pool)]
        return {"home": (pool, order, 12.3, 4.5, 8), "/alcohol/gin": (pool[:3], [3, 2, 1], 1.0, 1.0, 8)}

    def test_json_carries_every_column_per_page_with_the_timings(self, monkeypatch, capsys):
        monkeypatch.setattr(cli_featured, "gather", lambda db, pages, first: cli_featured.measure(self.loaded()))
        monkeypatch.setattr("app.db.SessionLocal", lambda: _FakeSession())
        assert cli.main(["featured", "evidence", "--json"]) == 0
        out = json.loads(capsys.readouterr().out)
        assert set(out) == {"home", "/alcohol/gin"}
        home = out["home"]
        for column in ("pool", "pictured", "bottles", "tiers", "awarded", "pinned", "pct_led", "usd_led", "both_led",
                       "median_usd", "median_pct", "pool_p75_usd", "pool_p75_pct", "brands", "categories",
                       "exclusives", "held_back", "gates", "ineligible", "hygiene", "targets", "slots"):
            assert column in home, column
        assert home["query_ms"] == 12.3 and home["order_ms"] == 4.5 and home["shown"] == 8
        assert out["/alcohol/gin"]["shown"] == 3

    def test_the_table_names_each_missed_target_and_verbose_lists_the_slots(self):
        pages = cli_featured.measure(self.loaded())
        text = cli_featured.render(pages)
        assert text.splitlines()[0].split() == ["home", "/alcohol/gin"]
        assert "targets met" in text and "home: missed" in text
        assert "  1. #11 $21.00 / 21% tier 3" in cli_featured.render(pages, verbose=True)

    def test_a_page_nobody_holds_is_exit_2(self, monkeypatch, capsys):
        monkeypatch.setattr(cli_featured, "gather", lambda db, pages, first: {})
        monkeypatch.setattr("app.db.SessionLocal", lambda: _FakeSession())
        assert cli.main(["featured", "evidence", "--pages", "/brands/nobody"]) == 2


class _FakeSession:
    def __enter__(self):
        return self

    def __exit__(self, *a):
        return False
