import pytest from batchmortal.tenhou import ( build_tenhou_paipu_urls, decode_tenhou_viewpoint, format_tenhou_timestamp, normalize_tenhou_modes, parse_tenhou_log_url, tenhou_mode_matches, tenhou_record_mode, ) PLAYER = "ププリン" def make_record( log_id: str, *, starttime: int, playernum: int = 4, playlength: int = 2, url_host: str = "tenhou.net", ) -> dict: record = { "starttime": starttime, "during": 32, "playernum": playernum, "playlength": playlength, "player1": PLAYER, "player2": "Tatsuno7", "player3": "Navitas", "tw": "198", "url": f"http://{url_host}/0/?log={log_id}", } if playernum == 4: record["player4"] = "tca00" return record def test_decode_nodocchi_viewpoint_from_real_example(): # Nodocchi returns 198 for this game. ププリン is player1 (first place), # and the site's two-bit permutation decoding resolves that to Tenhou seat 2. assert decode_tenhou_viewpoint("198", 1) == 2 def test_decode_tenhou_viewpoint_rejects_non_numeric_tw(): assert decode_tenhou_viewpoint("abc", 1) is None assert decode_tenhou_viewpoint(None, 1) is None @pytest.mark.parametrize("player_order", [0, 5, None]) def test_decode_tenhou_viewpoint_rejects_invalid_player_order(player_order): assert decode_tenhou_viewpoint("198", player_order) is None def test_decode_tenhou_viewpoint_returns_none_for_unmapped_packed_value(): assert decode_tenhou_viewpoint("999", 1) is None def test_build_real_example_paipu_url(): record = make_record( "2026071212gm-00a9-0000-2dab9d24", starttime=1783828560, ) assert build_tenhou_paipu_urls([record], PLAYER) == [ { "source": "tenhou", "mode": "4p-south", "uuid": "2026071212gm-00a9-0000-2dab9d24", "paipuUrl": "http://tenhou.net/0/?log=2026071212gm-00a9-0000-2dab9d24&tw=2", "startTime": format_tenhou_timestamp(1783828560), "endTime": format_tenhou_timestamp(1783828560 + 32 * 60), } ] def test_build_tenhou_paipu_urls_deduplicates_log_ids(): log_id = "2026071212gm-00a9-0000-11111111" records = [ make_record(log_id, starttime=200), make_record(log_id, starttime=100), ] items = build_tenhou_paipu_urls(records, PLAYER) assert len(items) == 1 assert items[0]["uuid"] == log_id assert items[0]["startTime"] == format_tenhou_timestamp(200) def test_build_tenhou_paipu_urls_skips_invalid_viewpoints(): valid_log_id = "2026071212gm-00a9-0000-11111111" invalid_tw = make_record( "2026071212gm-00a9-0000-22222222", starttime=200, ) invalid_tw["tw"] = "999" missing_player = make_record( "2026071212gm-00a9-0000-33333333", starttime=100, ) missing_player["player1"] = "another-player" items = build_tenhou_paipu_urls( [make_record(valid_log_id, starttime=300), invalid_tw, missing_player], PLAYER, ) assert [item["uuid"] for item in items] == [valid_log_id] def test_mode_filter_and_limit_are_applied_per_actual_mode(): records = [ make_record("2026071213gm-00a9-0000-aaaaaaaa", starttime=300, playlength=2), make_record("2026071212gm-00a9-0000-bbbbbbbb", starttime=200, playlength=2), make_record("2026071211gm-00a9-0000-cccccccc", starttime=100, playlength=1), ] items = build_tenhou_paipu_urls(records, PLAYER, modes="4p", limit=1) assert [item["uuid"] for item in items] == [ "2026071213gm-00a9-0000-aaaaaaaa", "2026071211gm-00a9-0000-cccccccc", ] assert [item["mode"] for item in items] == ["4p-south", "4p-east"] def test_non_tenhou_urls_are_not_mistaken_for_tenhou_records(): record = make_record( "2026071212gm-00a9-0000-2dab9d24", starttime=1783828560, url_host="game.maj-soul.com", ) assert build_tenhou_paipu_urls([record], PLAYER) == [] assert parse_tenhou_log_url(record["url"]) is None def test_records_without_url_or_viewpoint_are_skipped(): no_url = make_record("2026071212gm-00a9-0000-2dab9d24", starttime=200) no_url.pop("url") no_viewpoint = make_record("2026071213gm-00a9-0000-aaaaaaaa", starttime=300) no_viewpoint.pop("tw") assert build_tenhou_paipu_urls([no_url, no_viewpoint], PLAYER) == [] def test_tenhou_mode_aliases_and_labels(): assert normalize_tenhou_modes("四南,3p-east") == ("4p-south", "3p-east") assert normalize_tenhou_modes("*") == ("all",) assert tenhou_record_mode({"playernum": 3, "playlength": 1}) == "3p-east" def test_normalize_tenhou_modes_rejects_unsupported_label(): with pytest.raises(ValueError, match="Unsupported Tenhou mode 'foo'"): normalize_tenhou_modes(("foo",)) @pytest.mark.parametrize("modes", ["", (), ("",), (" ",), (" ", "", " ")]) def test_normalize_tenhou_modes_empty_values_fall_back_to_all(modes): assert normalize_tenhou_modes(modes) == ("all",) def test_tenhou_mode_matches_normalized_modes(): four_player = normalize_tenhou_modes(("4p",)) assert tenhou_mode_matches("4p-east", four_player) assert tenhou_mode_matches("4p-south", four_player) assert not tenhou_mode_matches("3p-east", four_player) assert not tenhou_mode_matches("3p-south", four_player) east_only = normalize_tenhou_modes(("4p-east",)) assert tenhou_mode_matches("4p-east", east_only) assert not tenhou_mode_matches("4p-south", east_only) south_only = normalize_tenhou_modes(("4p-south",)) assert tenhou_mode_matches("4p-south", south_only) assert not tenhou_mode_matches("4p-east", south_only)