"""Cross-device game-state sync: server-side merge so two devices converge.""" import pytest from goodnews import games from goodnews.db import connect, init_db @pytest.fixture def conn(tmp_path): c = connect(str(tmp_path / "t.db")) init_db(c) c.execute("INSERT INTO users (email) VALUES ('a@b.c')") c.commit() return c # --- merge logic (the audited core) --- def test_merge_wordsearch_unions_finds(): a = {"foundWords": [{"word": "CAT", "cells": [[0, 0]], "ci": 0}], "startTime": 100, "ms": 0} b = {"foundWords": [{"word": "DOG", "cells": [[1, 1]], "ci": 1}], "startTime": 50, "ms": 0} m = games.merge_game_state("wordsearch", a, b) assert {f["word"] for f in m["foundWords"]} == {"CAT", "DOG"} # union of finds assert m["startTime"] == 50 # earliest start def test_merge_wordsearch_dedupes_and_keeps_best_time(): a = {"foundWords": [{"word": "CAT", "cells": [[0, 0]], "ci": 0}], "ms": 5000} b = {"foundWords": [{"word": "CAT", "cells": [[0, 0]], "ci": 0}], "ms": 3000} m = games.merge_game_state("wordsearch", a, b) assert len(m["foundWords"]) == 1 and m["ms"] == 3000 # same word once, best time def test_merge_word_furthest_progress_wins(): playing = {"status": "playing", "guesses": ["AAAAA", "BBBBB"]} won = {"status": "won", "guesses": ["AAAAA", "CCCCC", "DDDDD"]} assert games.merge_game_state("word", playing, won) == won # terminal beats in-progress less = {"status": "playing", "guesses": ["A"]} more = {"status": "playing", "guesses": ["A", "B", "C"]} assert games.merge_game_state("word", less, more) == more # more guesses beats fewer def test_merge_handles_missing_sides(): a = {"status": "won", "guesses": ["x"]} assert games.merge_game_state("word", None, a) == a assert games.merge_game_state("word", a, None) == a # --- persistence convergence --- def test_save_converges_across_devices(conn): games.save_game_state(conn, 1, "wordsearch", "small", "2026-06-12", {"foundWords": [{"word": "CAT", "cells": [[0, 0]], "ci": 0}], "startTime": 100}) merged = games.save_game_state(conn, 1, "wordsearch", "small", "2026-06-12", {"foundWords": [{"word": "DOG", "cells": [[1, 1]], "ci": 1}], "startTime": 50}) assert {f["word"] for f in merged["foundWords"]} == {"CAT", "DOG"} # stored state reflects the merge (order-independent) assert games.load_game_state(conn, 1, "wordsearch", "small", "2026-06-12")["startTime"] == 50 # --- derived stats --- def test_word_stats_streak_and_distribution(conn): games.save_game_state(conn, 1, "word", "5", "2026-06-12", {"status": "won", "guesses": ["a", "b", "c"]}) games.save_game_state(conn, 1, "word", "5", "2026-06-11", {"status": "won", "guesses": ["a", "b", "c", "d"]}) games.save_game_state(conn, 1, "word", "5", "2026-06-10", {"status": "lost", "guesses": ["a"] * 6}) st = games.game_stats(conn, 1, "word", "5") assert st["played"] == 3 and st["won"] == 2 assert st["streak"] == 2 # two most-recent wins, then the loss stops it assert st["dist"] == {3: 1, 4: 1} def test_wordsearch_stats_best_time(conn): games.save_game_state(conn, 1, "wordsearch", "med", "2026-06-12", {"foundWords": [], "ms": 4000}) games.save_game_state(conn, 1, "wordsearch", "med", "2026-06-11", {"foundWords": [], "ms": 6000}) st = games.game_stats(conn, 1, "wordsearch", "med") assert st["completed"] == 2 and st["best"] == 4000 # --- API round-trip (signed-in only; needs the test helpers) --- def test_game_state_api_roundtrip(tmp_path, monkeypatch): from test_admin import _make, _signin from fastapi.testclient import TestClient app, api = _make(tmp_path, monkeypatch) # signed out → no sync, echoes the posted state and GET sees nothing stored anon = TestClient(app) body = {"game": "wordsearch", "variant": "small", "date": "2026-06-12", "state": {"foundWords": [{"word": "CAT", "cells": [[0, 0]], "ci": 0}], "startTime": 9}} assert anon.put("/api/games/state", json=body).json()["state"]["foundWords"][0]["word"] == "CAT" assert anon.get("/api/games/state?game=wordsearch&variant=small&date=2026-06-12").json()["state"] is None # signed in: push from "device A", then "device B" → server returns the union tc = _signin(app, api, "p@x.com") tc.put("/api/games/state", json=body) bodyB = {**body, "state": {"foundWords": [{"word": "DOG", "cells": [[1, 1]], "ci": 1}], "startTime": 4}} merged = tc.put("/api/games/state", json=bodyB).json()["state"] assert {f["word"] for f in merged["foundWords"]} == {"CAT", "DOG"} and merged["startTime"] == 4 # GET returns the stored merge; unknown game → 404 got = tc.get("/api/games/state?game=wordsearch&variant=small&date=2026-06-12").json()["state"] assert {f["word"] for f in got["foundWords"]} == {"CAT", "DOG"} assert tc.get("/api/games/state?game=nope&variant=x&date=2026-06-12").status_code == 404