"""Enrich-stage tests: full-metadata parsing plus run_enrich orchestration
(batched, cache-keyed, idempotent, token-degrading). No network."""

from __future__ import annotations

import json

import httpx
import pytest

from bggpipe.bgg_client import BGGClient, cache_key
from bggpipe.config import Config
from bggpipe.models import parse_things_full
from bggpipe.resolve import write_matches

FULL_THING_XML = """<items>
  <item type="boardgame" id="266192">
    <thumbnail>https://cf.example/thumb.jpg</thumbnail>
    <image>https://cf.example/full.jpg</image>
    <name type="primary" sortindex="1" value="Wingspan"/>
    <name type="alternate" value="Fl&#252;gelschlag"/>
    <description>A bird-collection engine builder.</description>
    <yearpublished value="2019"/>
    <minplayers value="1"/>
    <maxplayers value="5"/>
    <poll name="suggested_numplayers">
      <results numplayers="1">
        <result value="Best" numvotes="10"/>
        <result value="Recommended" numvotes="50"/>
        <result value="Not Recommended" numvotes="20"/>
      </results>
      <results numplayers="3">
        <result value="Best" numvotes="120"/>
        <result value="Recommended" numvotes="40"/>
        <result value="Not Recommended" numvotes="2"/>
      </results>
      <results numplayers="4">
        <result value="Best" numvotes="90"/>
        <result value="Recommended" numvotes="80"/>
        <result value="Not Recommended" numvotes="5"/>
      </results>
    </poll>
    <playingtime value="70"/>
    <minplaytime value="40"/>
    <maxplaytime value="70"/>
    <minage value="10"/>
    <link type="boardgamecategory" id="1" value="Animals"/>
    <link type="boardgamecategory" id="2" value="Card Game"/>
    <link type="boardgamemechanic" id="3" value="Engine Building"/>
    <link type="boardgamedesigner" id="4" value="Elizabeth Hargrave"/>
    <link type="boardgameartist" id="5" value="Natalia Rojas"/>
    <link type="boardgamepublisher" id="6" value="Stonemaier Games"/>
    <statistics page="1"><ratings>
      <average value="8.05"/>
      <averageweight value="2.45"/>
      <owned value="123456"/>
      <ranks><rank type="subtype" id="1" name="boardgame" value="30"/></ranks>
    </ratings></statistics>
  </item>
</items>"""


def test_parse_things_full_extracts_everything():
    (game,) = parse_things_full(FULL_THING_XML)
    assert game["name"] == "Wingspan"
    assert game["year"] == 2019
    assert game["description"] == "A bird-collection engine builder."
    assert (game["min_players"], game["max_players"]) == (1, 5)
    # 3 is Best-majority; 4 loses to Recommended+NotRec? no — Best(90) >= Rec(80)
    # and > NotRec(5), so 4 qualifies too; 1 does not (Best < Recommended)
    assert game["best_player_counts"] == ["3", "4"]
    assert game["designers"] == ["Elizabeth Hargrave"]
    assert game["artists"] == ["Natalia Rojas"]
    assert game["publishers"] == ["Stonemaier Games"]
    assert game["categories"] == ["Animals", "Card Game"]
    assert game["mechanics"] == ["Engine Building"]
    assert game["rating"] == 8.05
    assert game["weight"] == 2.45
    assert game["rank"] == 30
    assert game["playtime"] == 70
    assert game["min_age"] == 10
    assert game["image"].endswith("full.jpg")


CATAN_MINIMAL = (
    '<items><item type="boardgame" id="13">'
    '<name type="primary" value="CATAN"/><yearpublished value="1995"/>'
    "</item></items>"
)


def _matches_rows():
    base = {
        "year": "",
        "type": "boardgame",
        "candidates_json": "[]",
        "source_photos": "x.jpg",
        "version_id": "",
        "version_name": "",
        "version_status": "version_unknown",
        "version_candidates_json": "[]",
    }
    return [
        {
            **base,
            "title_raw": "Catan",
            "bgg_id": "13",
            "bgg_name": "CATAN",
            "match_status": "auto",
        },
        {
            **base,
            "title_raw": "Wingspan",
            "bgg_id": "266192",
            "bgg_name": "Wingspan",
            "match_status": "auto",
            "version_id": "465063",
            "version_name": "English first edition",
            "version_status": "version_auto",
            "version_candidates_json": json.dumps(
                [
                    {
                        "version_id": 465063,
                        "name": "English first edition",
                        "year": 2019,
                        "publishers": ["Stonemaier Games"],
                        "languages": ["English"],
                        "score": 5,
                    }
                ]
            ),
        },
        {
            **base,
            "title_raw": "Junk",
            "bgg_id": "",
            "bgg_name": "",
            "match_status": "rejected",
        },
    ]


def _no_network(request: httpx.Request) -> httpx.Response:
    raise AssertionError(f"test hit the network: {request.url}")


def _cfg_with_matches(tmp_path):
    cfg = Config(data_dir=tmp_path / "data")
    write_matches(cfg.matches_path, _matches_rows())
    return cfg


def _seed_batch_fixture(cache_dir):
    """The cache entries run_enrich will ask for: the stats batch and the
    dims pass's versions batch (sorted ids 13,266192)."""
    cache_dir.mkdir(parents=True, exist_ok=True)
    combined = FULL_THING_XML.replace("<items>", "<items>" + CATAN_MINIMAL[7:-8], 1)
    key = cache_key("thing", {"id": "13,266192", "stats": "1"})
    (cache_dir / key).write_text(combined)
    versions_key = cache_key("thing", {"id": "13,266192", "versions": "1"})
    (cache_dir / versions_key).write_text(
        """<items>
          <item type="boardgame" id="13"><versions>
            <item type="boardgameversion" id="7"><width value="9.5" />
              <length value="11.5" /><depth value="3" /><weight value="3" /></item>
          </versions></item>
          <item type="boardgame" id="266192"><versions>
            <item type="boardgameversion" id="465063"><width value="11.7" />
              <length value="11.7" /><depth value="2.8" /><weight value="4.4" /></item>
          </versions></item>
        </items>"""
    )


def test_run_enrich_writes_games_json_with_versions(tmp_path):
    from bggpipe.enrich import run_enrich

    cfg = _cfg_with_matches(tmp_path)
    cache = tmp_path / "cache"
    _seed_batch_fixture(cache)
    client = BGGClient(cache_dir=cache, transport=httpx.MockTransport(_no_network))

    games = run_enrich(cfg, client=client)
    assert set(games) == {"13", "266192:465063"}
    wingspan = games["266192:465063"]
    assert wingspan["designers"] == ["Elizabeth Hargrave"]
    assert wingspan["version"]["name"] == "English first edition"
    assert wingspan["version"]["publishers"] == ["Stonemaier Games"]
    assert games["13"]["version"] is None
    saved = json.loads((cfg.data_dir / "games.json").read_text())
    assert saved == games


def test_run_enrich_skips_already_enriched(tmp_path):
    from bggpipe.enrich import run_enrich

    cfg = _cfg_with_matches(tmp_path)
    cache = tmp_path / "cache"
    _seed_batch_fixture(cache)
    client = BGGClient(cache_dir=cache, transport=httpx.MockTransport(_no_network))
    run_enrich(cfg, client=client)

    # empty cache + network-refusing transport: passes only if enrich
    # doesn't need to fetch anything at all
    bare = BGGClient(
        cache_dir=tmp_path / "empty", transport=httpx.MockTransport(_no_network)
    )
    games = run_enrich(cfg, client=bare)
    assert set(games) == {"13", "266192:465063"}


def test_refresh_bypasses_cache_read(tmp_path):
    from bggpipe.enrich import run_enrich

    cfg = _cfg_with_matches(tmp_path)
    cache = tmp_path / "cache"
    _seed_batch_fixture(cache)
    requests = []

    def handler(request):
        requests.append(str(request.url))
        return httpx.Response(200, text=FULL_THING_XML.replace("8.05", "7.5"))

    client = BGGClient(cache_dir=cache, transport=httpx.MockTransport(handler))
    games = run_enrich(cfg, refresh=True, client=client)
    # cache reads skipped, live fetches happened: stats batch + dims pass
    assert any("stats=1" in u for u in requests)
    assert any("versions=1" in u for u in requests)
    assert games["266192:465063"]["rating"] == 7.5


def test_enrich_degrades_without_token(tmp_path, capsys):
    from bggpipe.enrich import run_enrich

    cfg = _cfg_with_matches(tmp_path)
    client = BGGClient(
        cache_dir=tmp_path / "empty",
        transport=httpx.MockTransport(
            lambda req: httpx.Response(401, text="Unauthorized")
        ),
    )
    games = run_enrich(cfg, client=client)
    assert games == {}
    assert "waiting on the BGG API" in capsys.readouterr().out
    assert json.loads((cfg.data_dir / "games.json").read_text()) == {}


def test_local_rows_enrich_from_their_own_reads(tmp_path):
    cfg = Config(data_dir=tmp_path / "data")
    cfg.data_dir.mkdir(parents=True)
    write_matches(
        cfg.matches_path,
        [
            {
                "title_raw": "Obscure Homebrew",
                "bgg_id": "",
                "bgg_name": "",
                "year": "",
                "type": "",
                "match_status": "local",
                "version_id": "",
                "version_name": "",
                "version_status": "",
                "candidates_json": "[]",
                "version_candidates_json": "[]",
                "source_photos": "shelf.jpg",
            }
        ],
    )
    cfg.titles_path.write_text(
        json.dumps(
            [
                {
                    "title_raw": "Obscure Homebrew",
                    "publisher_hint": "Basement Press",
                    "year_hint": 1998,
                    "source_photos": ["shelf.jpg"],
                }
            ]
        )
    )
    from bggpipe.enrich import run_enrich

    client = BGGClient(
        cache_dir=cfg.data_dir / "cache",
        transport=httpx.MockTransport(_no_network),
    )
    games = run_enrich(cfg, client=client)
    (key,) = [k for k in games if k.startswith("local:")]
    entry = games[key]
    assert entry["name"] == "Obscure Homebrew"
    assert entry["year"] == 1998
    assert entry["publishers"] == ["Basement Press"]
    assert entry["type"] == "localgame"
    # idempotent: a second run keeps the entry (no prune, no dupe)
    games2 = run_enrich(cfg, client=client)
    assert key in games2


def test_summary_counts_local_and_api_entries_separately(tmp_path, capsys):
    """Local entries have no API target, so folding them into the fetched
    tally made "already present or waiting" report a NEGATIVE count."""
    from bggpipe.enrich import run_enrich

    cfg = Config(data_dir=tmp_path / "data")
    cfg.data_dir.mkdir(parents=True)
    write_matches(
        cfg.matches_path,
        [
            {
                "title_raw": "Homebrew",
                "bgg_id": "",
                "bgg_name": "",
                "year": "",
                "type": "",
                "match_status": "local",
                "version_id": "",
                "version_name": "",
                "version_status": "",
                "candidates_json": "[]",
                "version_candidates_json": "[]",
                "source_photos": "shelf.jpg",
            }
        ],
    )
    cfg.titles_path.write_text("[]")
    client = BGGClient(
        cache_dir=cfg.data_dir / "cache",
        transport=httpx.MockTransport(_no_network),
    )
    games = run_enrich(cfg, client=client)
    out = capsys.readouterr().out
    assert len(games) == 1
    assert "1 local-only" in out
    import re as _re

    assert not _re.search(r"-\d", out)  # no negative tallies


def test_corrupt_local_games_store_fails_loud(tmp_path):
    """local_games.json is the ONLY source for off-BGG games — a tolerant
    reader that skipped it would silently drop hand-written metadata."""
    from bggpipe.enrich import run_enrich

    cfg = Config(data_dir=tmp_path / "data")
    rows = _matches_rows()
    for row in rows:  # all local: the store must be read before any fetch
        row["match_status"] = "local"
        row["bgg_id"] = ""
    write_matches(cfg.matches_path, rows)
    cfg.local_games_path.write_text("{torn")
    bare = BGGClient(
        cache_dir=tmp_path / "empty", transport=httpx.MockTransport(_no_network)
    )
    with pytest.raises(ValueError, match="local_games.json is corrupt"):
        run_enrich(cfg, client=bare)


DIMS_XML = """<items>
  <item type="boardgame" id="240">
    <versions>
      <item type="boardgameversion" id="24621">
        <width value="8.4" /><length value="11.5" /><depth value="2.1" />
        <weight value="2" />
      </item>
      <item type="boardgameversion" id="99999">
        <width value="0" /><length value="0" /><depth value="0" />
        <weight value="0" />
      </item>
    </versions>
  </item>
  <item type="boardgame" id="500">
    <versions>
      <item type="boardgameversion" id="1"><width value="11.6" />
        <length value="11.6" /><depth value="2.8" /><weight value="4.1" /></item>
      <item type="boardgameversion" id="2"><width value="11.8" />
        <length value="11.4" /><depth value="3.0" /><weight value="4.3" /></item>
      <item type="boardgameversion" id="3"><width value="0" />
        <length value="0" /><depth value="0" /><weight value="0" /></item>
    </versions>
  </item>
  <item type="boardgame" id="600">
    <versions>
      <item type="boardgameversion" id="4"><width value="8.0" />
        <length value="8.0" /><depth value="2.0" /><weight value="1" /></item>
      <item type="boardgameversion" id="5"><width value="12.0" />
        <length value="12.0" /><depth value="3.0" /><weight value="5" /></item>
    </versions>
  </item>
  <item type="boardgame" id="700">
    <versions>
      <item type="boardgameversion" id="6"><width value="16.2" />
        <length value="16.2" /><depth value="4.0" /><weight value="7" /></item>
    </versions>
  </item>
</items>"""


def test_parse_version_dims_treats_zero_as_absent():
    from bggpipe.models import parse_version_dims

    by_game = parse_version_dims(DIMS_XML)
    exact = next(r for r in by_game[240] if r["version_id"] == 24621)
    assert exact["width_in"] == 8.4 and exact["weight_lb"] == 2
    zeroed = next(r for r in by_game[240] if r["version_id"] == 99999)
    assert all(
        zeroed[f] is None for f in ("width_in", "length_in", "depth_in", "weight_lb")
    )


def test_entry_dims_verdicts():
    """The four sources: exact version, unanimous chorus (within 0.5"),
    conflicting chorus (nulls, never a guess), and absent."""
    from bggpipe.enrich import _entry_dims
    from bggpipe.models import parse_version_dims

    by_game = parse_version_dims(DIMS_XML)

    versioned = _entry_dims({"version": {"version_id": 24621}}, by_game[240])
    assert versioned["source"] == "version" and versioned["width_in"] == 8.4

    # chosen version exists but carries only zeros: absent, not borrowed
    zeroed = _entry_dims({"version": {"version_id": 99999}}, by_game[240])
    assert zeroed["source"] == "absent" and zeroed["width_in"] is None

    unanimous = _entry_dims({"version": None}, by_game[500])
    assert unanimous["source"] == "unanimous"
    assert unanimous["width_in"] == 11.8  # max per axis: the fit question
    assert unanimous["depth_in"] == 3.0

    conflicting = _entry_dims({"version": None}, by_game[600])
    assert conflicting["source"] == "conflicting"
    assert conflicting["width_in"] is None

    assert _entry_dims({"version": None}, [])["source"] == "absent"


def test_enrich_fills_dims_from_cached_versions(tmp_path):
    from bggpipe.enrich import run_enrich

    cfg = _cfg_with_matches(tmp_path)
    cache = tmp_path / "cache"
    _seed_batch_fixture(cache)
    (cache / cache_key("thing", {"id": "13,266192", "versions": "1"})).write_text(
        """<items>
          <item type="boardgame" id="13"><versions>
            <item type="boardgameversion" id="7"><width value="11.6" />
              <length value="11.6" /><depth value="3.0" /><weight value="4" /></item>
          </versions></item>
          <item type="boardgame" id="266192"><versions>
            <item type="boardgameversion" id="465063"><width value="11.7" />
              <length value="11.7" /><depth value="2.8" /><weight value="4.4" /></item>
          </versions></item>
        </items>"""
    )
    client = BGGClient(cache_dir=cache, transport=httpx.MockTransport(_no_network))
    games = run_enrich(cfg, client=client)
    wingspan = games["266192:465063"]
    assert wingspan["dims"]["source"] == "version"
    assert wingspan["dims"]["depth_in"] == 2.8
    assert wingspan["version"]["width_in"] == 11.7  # mirrored onto the version
    assert games["13"]["dims"]["source"] == "unanimous"
