"""Memory Match server state — light, durability-only (no anti-cheat; the board is
deterministic and fully visible). Locks: malformed keys dropped, matched stored as
deduped face KEYS, `done` DERIVED from the matched count vs the tier's target (never
trusted from the client), cross-device merge unions matched, and the sync endpoint
accepts only valid match variants."""
import pytest
from fastapi.testclient import TestClient

from goodnews import games
from goodnews.db import connect, init_db


def _san(variant, state):
    return games.sanitize_game_state(None, "match", variant, "2026-06-16", state)


def test_sanitize_drops_junk_and_dedupes():
    s = _san("standard-icons", {
        "matched": ["leaf", "leaf", "color-rose", "banana", "BAD KEY!", 42, "x" * 40, "sun"],
        "moves": -5, "done": "yes",
    })
    # deduped + validated against the real face set ("banana"/junk dropped), order kept
    assert s["matched"] == ["leaf", "color-rose", "sun"]
    assert s["moves"] == 0                                 # clamped ≥ 0
    assert s["done"] is False                              # 3 < 8 faces — client's "yes" ignored


def test_done_is_derived_from_matched_count_not_client_flag():
    real8 = ["leaf", "sun", "star", "moon", "cloud", "wave", "tree", "heart"]   # real faces
    # client lies that it's done with no progress → server says not done
    assert _san("standard-icons", {"matched": [], "done": True})["done"] is False
    # reaching the tier's face target (standard = 8) → done
    assert _san("standard-icons", {"matched": real8, "done": False})["done"] is True
    # gentle target is only 6
    assert _san("gentle-icons", {"matched": real8[:6]})["done"] is True
    assert _san("standard-icons", {"matched": real8[:6]})["done"] is False


def test_sanitize_caps_face_count():
    many = ["color-rose", "color-coral", "color-amber", "color-gold", "color-lime",
            "color-green", "color-teal", "color-cyan", "color-sky", "color-blue",
            "color-indigo", "color-violet", "color-plum", "color-brown", "color-sand"]  # 15 real
    s = _san("expert-colors", {"matched": many})
    assert len(s["matched"]) == 12                          # _MATCH_MAX_FACES


def test_merge_unions_matched_and_keeps_moves_without_trusting_done():
    a = {"matched": ["leaf", "sun"], "moves": 7, "done": False}
    b = {"matched": ["sun", "star"], "moves": 4, "done": True}
    m = games.merge_game_state("match", a, b)
    assert sorted(m["matched"]) == ["leaf", "star", "sun"]  # union
    assert m["moves"] == 7                                  # larger move count
    assert "done" not in m                                  # merge doesn't carry done; sanitize derives it


@pytest.fixture
def api_app(tmp_path, monkeypatch):
    db = tmp_path / "t.sqlite3"
    monkeypatch.setenv("GOODNEWS_DB", str(db))
    monkeypatch.setenv("GOODNEWS_PUBLIC_BASE_URL", "http://testserver")
    monkeypatch.setenv("GOODNEWS_ADMIN_EMAILS", "admin@b.com")
    import importlib
    import goodnews.api as api
    importlib.reload(api)
    c = connect(str(db)); init_db(c); c.commit(); c.close()
    return api.create_app()


def _signin(app, email="p@b.com"):
    tc = TestClient(app)
    sent = {}
    import goodnews.email_send as es
    orig = es.send_magic_link
    es.send_magic_link = lambda to, link: sent.update(link=link)
    try:
        tc.post("/api/auth/email/start", json={"email": email})
        tc.post("/api/auth/email/verify", json={"token": sent["link"].split("token=")[1]})
    finally:
        es.send_magic_link = orig
    return tc


def _put(tc, variant, state):
    return tc.put("/api/games/state", json={
        "game": "match", "variant": variant, "date": "2026-06-16", "state": state})


def test_sync_endpoint_flow(api_app):
    tc = _signin(api_app)
    r1 = _put(tc, "standard-icons", {"matched": ["leaf", "sun"], "moves": 3, "done": False})
    assert r1.status_code == 200
    assert sorted(r1.json()["state"]["matched"]) == ["leaf", "sun"]
    assert r1.json()["state"]["done"] is False
    # a second device merges in (still partial → not done)
    r2 = _put(tc, "standard-icons", {"matched": ["star"], "moves": 1, "done": True})
    assert sorted(r2.json()["state"]["matched"]) == ["leaf", "star", "sun"]
    assert r2.json()["state"]["done"] is False              # 3 < 8, client's done ignored
    # completing the board (8 real faces) → done
    r3 = _put(tc, "standard-icons",
              {"matched": ["leaf", "sun", "star", "moon", "cloud", "wave", "tree", "heart"], "moves": 12})
    assert r3.json()["state"]["done"] is True
    # unknown variant rejected
    assert _put(tc, "huge-icons", {}).status_code == 404


def test_batch_endpoint_reconciles_many_and_drops_bad(api_app):
    tc = _signin(api_app)
    body = {"date": "2026-06-16", "items": [
        {"game": "match", "variant": "standard-icons", "state": {"matched": ["leaf", "sun"], "moves": 2}},
        {"game": "bloom", "variant": "", "state": {"found": []}},
        {"game": "match", "variant": "bogus-xyz", "state": {}},        # unknown variant → dropped
    ]}
    r = tc.put("/api/games/state/batch", json=body)
    assert r.status_code == 200
    states = r.json()["states"]
    variants = {(s["game"], s["variant"]) for s in states}
    assert ("match", "standard-icons") in variants
    assert ("bloom", "") in variants
    assert ("match", "bogus-xyz") not in variants            # invalid item dropped, not fatal
    m = next(s for s in states if s["variant"] == "standard-icons")
    assert sorted(m["state"]["matched"]) == ["leaf", "sun"]   # merged + sanitized
    # a second device merges via the same batch path
    r2 = tc.put("/api/games/state/batch", json={"date": "2026-06-16", "items": [
        {"game": "match", "variant": "standard-icons", "state": {"matched": ["star"], "moves": 5}}]})
    m2 = r2.json()["states"][0]["state"]
    assert sorted(m2["matched"]) == ["leaf", "star", "sun"] and m2["moves"] == 5


def test_batch_endpoint_signed_out_echoes(api_app):
    from fastapi.testclient import TestClient
    r = TestClient(api_app).put("/api/games/state/batch", json={"date": "2026-06-16", "items": [
        {"game": "match", "variant": "gentle-colors", "state": {"matched": ["color-rose"]}}]})
    assert r.status_code == 200
    assert r.json()["states"][0]["state"] == {"matched": ["color-rose"]}  # echo, no sync
