File size: 22,426 Bytes
f1fd35c
 
 
 
 
 
 
 
 
9c7fde6
 
f1fd35c
 
 
 
 
 
 
 
 
9c7fde6
f1fd35c
 
 
 
 
 
 
9c7fde6
 
 
f1fd35c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9c7fde6
f1fd35c
 
9c7fde6
f1fd35c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9c7fde6
f1fd35c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9c7fde6
f1fd35c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9c7fde6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f1fd35c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9c7fde6
 
 
f1fd35c
 
9c7fde6
 
 
f1fd35c
 
 
 
 
 
 
 
9c7fde6
 
 
f1fd35c
 
 
 
 
 
 
 
 
 
 
9c7fde6
 
 
f1fd35c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9c7fde6
f1fd35c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9c7fde6
 
 
f1fd35c
9c7fde6
 
 
f1fd35c
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
# tests/test_leaderboard.py
"""
Unit tests for the Wrdler Leaderboard System.

Tests cover:
- UserEntry and LeaderboardSettings dataclasses
- Qualification logic
- Sorting functions
- Date/week ID generation
- File ID generation and parsing
- GameSettings matching
"""

import pytest
from datetime import datetime, timezone, timedelta
from unittest.mock import patch, MagicMock

from wrdler.leaderboard import (
    UserEntry,
    LeaderboardSettings,
    GameSettings,
    get_current_daily_id,
    get_current_weekly_id,
    get_daily_leaderboard_path,
    get_weekly_leaderboard_path,
    _sort_users,
    check_qualification,
    create_user_entry,
    _sanitize_wordlist_source,
    _build_file_id,
    _parse_file_id,
    MAX_DISPLAY_ENTRIES,
)


class TestUserEntry:
    """Tests for UserEntry dataclass."""

    def test_create_entry(self):
        """Test basic UserEntry creation."""
        entry = UserEntry(
            uid="test-uid-123",
            username="TestPlayer",
            word_list=["WORD", "TEST", "GAME", "PLAY", "SCORE", "FINAL"],
            score=42,
            time=180,
            timestamp="2025-01-27T12:00:00+00:00",
        )
        assert entry.uid == "test-uid-123"
        assert entry.username == "TestPlayer"
        assert len(entry.word_list) == 6
        assert entry.score == 42
        assert entry.time == 180
        assert entry.word_list_difficulty is None
        assert entry.source_challenge_id is None

    def test_to_dict_basic(self):
        """Test to_dict without optional fields."""
        entry = UserEntry(
            uid="test-uid",
            username="Player",
            word_list=["A", "B", "C", "D", "E", "F"],
            score=30,
            time=120,
            timestamp="2025-01-27T12:00:00+00:00",
        )
        d = entry.to_dict()
        
        assert d["uid"] == "test-uid"
        assert d["username"] == "Player"
        assert d["score"] == 30
        assert d["time"] == 120
        assert "word_list_difficulty" not in d
        assert "source_challenge_id" not in d

    def test_to_dict_with_optional_fields(self):
        """Test to_dict with optional fields."""
        entry = UserEntry(
            uid="test-uid",
            username="Player",
            word_list=["A", "B", "C", "D", "E", "F"],
            score=30,
            time=120,
            timestamp="2025-01-27T12:00:00+00:00",
            word_list_difficulty=117.5,
            source_challenge_id="challenge-123",
        )
        d = entry.to_dict()
        
        assert d["word_list_difficulty"] == 117.5
        assert d["source_challenge_id"] == "challenge-123"

    def test_from_dict_roundtrip(self):
        """Test to_dict/from_dict roundtrip."""
        original = UserEntry(
            uid="test-uid",
            username="Player",
            word_list=["A", "B", "C", "D", "E", "F"],
            score=30,
            time=120,
            timestamp="2025-01-27T12:00:00+00:00",
            word_list_difficulty=100.0,
        )
        d = original.to_dict()
        restored = UserEntry.from_dict(d)
        
        assert restored.uid == original.uid
        assert restored.username == original.username
        assert restored.score == original.score
        assert restored.time == original.time
        assert restored.word_list_difficulty == original.word_list_difficulty

    def test_from_dict_legacy_time_seconds(self):
        """Test from_dict handles legacy 'time_seconds' field."""
        data = {
            "uid": "test",
            "username": "Player",
            "word_list": ["A", "B", "C", "D", "E", "F"],
            "score": 30,
            "time_seconds": 150,  # Legacy field name
            "timestamp": "2025-01-27T12:00:00+00:00",
        }
        entry = UserEntry.from_dict(data)
        assert entry.time == 150


class TestLeaderboardSettings:
    """Tests for LeaderboardSettings dataclass."""

    def test_create_leaderboard(self):
        """Test basic LeaderboardSettings creation."""
        lb = LeaderboardSettings(
            challenge_id="2025-01-27/classic-classic-0",
            entry_type="daily",
        )
        assert lb.challenge_id == "2025-01-27/classic-classic-0"
        assert lb.entry_type == "daily"
        assert lb.game_mode == "classic"
        assert lb.grid_size == 8
        assert len(lb.users) == 0
        assert lb.max_display_entries == MAX_DISPLAY_ENTRIES

    def test_entry_type_values(self):
        """Test valid entry_type values."""
        for entry_type in ["daily", "weekly", "challenge"]:
            lb = LeaderboardSettings(
                challenge_id="test",
                entry_type=entry_type,
            )
            assert lb.entry_type == entry_type

    def test_get_display_users_limit(self):
        """Test get_display_users respects max_display_entries."""
        users = [
            UserEntry(
                uid=f"uid-{i}",
                username=f"Player{i}",
                word_list=["A", "B", "C", "D", "E", "F"],
                score=100 - i,
                time=60 + i,
                timestamp="2025-01-27T12:00:00+00:00",
            )
            for i in range(25)  # More than MAX_DISPLAY_ENTRIES
        ]
        
        lb = LeaderboardSettings(
            challenge_id="test",
            entry_type="daily",
            users=users,
        )
        
        display_users = lb.get_display_users()
        assert len(display_users) == MAX_DISPLAY_ENTRIES
        # Should be first 20 (already sorted by creation)
        assert display_users[0].uid == "uid-0"

    def test_to_dict_and_from_dict(self):
        """Test LeaderboardSettings serialization roundtrip."""
        user = UserEntry(
            uid="test-uid",
            username="Player",
            word_list=["A", "B", "C", "D", "E", "F"],
            score=50,
            time=100,
            timestamp="2025-01-27T12:00:00+00:00",
        )
        
        lb = LeaderboardSettings(
            challenge_id="2025-01-27/easy-easy-0",
            entry_type="daily",
            game_mode="easy",
            users=[user],
            wordlist_source="test.txt",
        )
        
        d = lb.to_dict()
        restored = LeaderboardSettings.from_dict(d)
        
        assert restored.challenge_id == lb.challenge_id
        assert restored.entry_type == lb.entry_type
        assert restored.game_mode == lb.game_mode
        assert len(restored.users) == 1
        assert restored.wordlist_source == lb.wordlist_source

    def test_format_matches_challenge_structure(self):
        """Test that leaderboard format matches challenge settings.json structure."""
        lb = LeaderboardSettings(
            challenge_id="2025-01-27/classic-classic-0",
            entry_type="daily",
            game_mode="classic",
            grid_size=8,
            wordlist_source="classic.txt",
        )
        d = lb.to_dict()
        
        # Key fields that should match challenge format
        assert "challenge_id" in d
        assert "entry_type" in d
        assert "game_mode" in d
        assert "grid_size" in d
        assert "puzzle_options" in d
        assert "users" in d
        assert "created_at" in d
        assert "version" in d
        assert "show_incorrect_guesses" in d
        assert "enable_free_letters" in d
        assert "wordlist_source" in d


class TestGameSettings:
    """Tests for GameSettings dataclass."""

    def test_create_default_settings(self):
        """Test default GameSettings creation."""
        settings = GameSettings()
        assert settings.game_mode == "classic"
        assert settings.wordlist_source == "classic.txt"
        assert settings.show_incorrect_guesses is True
        assert settings.enable_free_letters is True

    def test_settings_matching_same(self):
        """Test that identical settings match."""
        s1 = GameSettings(game_mode="classic", wordlist_source="classic.txt")
        s2 = GameSettings(game_mode="classic", wordlist_source="classic.txt")
        assert s1.matches(s2) is True

    def test_settings_matching_different_mode(self):
        """Test that different game modes don't match."""
        s1 = GameSettings(game_mode="classic", wordlist_source="classic.txt")
        s2 = GameSettings(game_mode="easy", wordlist_source="classic.txt")
        assert s1.matches(s2) is False

    def test_settings_matching_different_wordlist(self):
        """Test that different wordlists don't match."""
        s1 = GameSettings(game_mode="classic", wordlist_source="classic.txt")
        s2 = GameSettings(game_mode="classic", wordlist_source="easy.txt")
        assert s1.matches(s2) is False

    def test_settings_matching_txt_extension_ignored(self):
        """Test that .txt extension is ignored in comparison."""
        s1 = GameSettings(game_mode="classic", wordlist_source="classic.txt")
        s2 = GameSettings(game_mode="classic", wordlist_source="classic")
        # Both should have same sanitized source
        assert s1._get_sanitized_source() == s2._get_sanitized_source()

    def test_get_file_id_prefix(self):
        """Test file_id prefix generation."""
        settings = GameSettings(game_mode="classic", wordlist_source="classic.txt")
        assert settings.get_file_id_prefix() == "classic-classic"

        settings2 = GameSettings(game_mode="too easy", wordlist_source="easy.txt")
        assert settings2.get_file_id_prefix() == "easy-too_easy"


class TestFileIdFunctions:
    """Tests for file ID generation and parsing."""

    def test_sanitize_wordlist_source_removes_txt(self):
        """Test that .txt extension is removed."""
        assert _sanitize_wordlist_source("classic.txt") == "classic"
        assert _sanitize_wordlist_source("easy.txt") == "easy"
        assert _sanitize_wordlist_source("my_words.txt") == "my_words"

    def test_sanitize_wordlist_source_lowercase(self):
        """Test that output is lowercase."""
        assert _sanitize_wordlist_source("CLASSIC.txt") == "classic"
        assert _sanitize_wordlist_source("MyWords.TXT") == "mywords"

    def test_sanitize_wordlist_source_no_extension(self):
        """Test sources without .txt extension."""
        assert _sanitize_wordlist_source("classic") == "classic"

    def test_build_file_id(self):
        """Test file_id building."""
        assert _build_file_id("classic.txt", "classic", 0) == "classic-classic-0"
        assert _build_file_id("easy.txt", "easy", 1) == "easy-easy-1"
        assert _build_file_id("classic.txt", "too easy", 2) == "classic-too_easy-2"

    def test_parse_file_id(self):
        """Test file_id parsing."""
        source, mode, seq = _parse_file_id("classic-classic-0")
        assert source == "classic"
        assert mode == "classic"
        assert seq == 0

        source, mode, seq = _parse_file_id("easy-too_easy-5")
        assert source == "easy"
        assert mode == "too easy"
        assert seq == 5

    def test_parse_file_id_invalid(self):
        """Test file_id parsing with invalid format."""
        with pytest.raises(ValueError):
            _parse_file_id("invalid")
        
        with pytest.raises(ValueError):
            _parse_file_id("classic-classic-notanumber")


class TestQualification:
    """Tests for qualification logic."""

    def test_qualify_empty_leaderboard(self):
        """Test that any score qualifies for empty leaderboard."""
        assert check_qualification(None, 1, 999) is True

    def test_qualify_not_full(self):
        """Test qualification when leaderboard is not full."""
        users = [
            UserEntry(
                uid=f"uid-{i}",
                username=f"Player{i}",
                word_list=["A", "B", "C", "D", "E", "F"],
                score=50,
                time=100,
                timestamp="2025-01-27T12:00:00+00:00",
            )
            for i in range(10)  # Less than MAX_DISPLAY_ENTRIES
        ]
        
        lb = LeaderboardSettings(
            challenge_id="test",
            entry_type="daily",
            users=users,
        )
        
        # Any score should qualify
        assert check_qualification(lb, 1, 999) is True

    def test_qualify_by_score(self):
        """Test qualification by higher score."""
        users = [
            UserEntry(
                uid=f"uid-{i}",
                username=f"Player{i}",
                word_list=["A", "B", "C", "D", "E", "F"],
                score=50 - i,  # Scores from 50 down to 31
                time=100,
                timestamp="2025-01-27T12:00:00+00:00",
            )
            for i in range(20)
        ]
        
        lb = LeaderboardSettings(
            challenge_id="test",
            entry_type="daily",
            users=users,
        )
        
        # Higher than lowest (31) should qualify
        assert check_qualification(lb, 32, 100) is True
        # Equal to lowest but faster time should qualify
        assert check_qualification(lb, 31, 99) is True
        # Lower than lowest should not qualify
        assert check_qualification(lb, 30, 100) is False

    def test_qualify_by_time_tiebreaker(self):
        """Test qualification using time as tiebreaker."""
        users = [
            UserEntry(
                uid=f"uid-{i}",
                username=f"Player{i}",
                word_list=["A", "B", "C", "D", "E", "F"],
                score=50,  # All same score
                time=100 + i,  # Times from 100 to 119
                timestamp="2025-01-27T12:00:00+00:00",
            )
            for i in range(20)
        ]
        
        lb = LeaderboardSettings(
            challenge_id="test",
            entry_type="daily",
            users=users,
        )
        
        # Same score but faster than slowest (119) should qualify
        assert check_qualification(lb, 50, 118) is True
        # Same score and slower should not qualify
        assert check_qualification(lb, 50, 120) is False

    def test_qualify_by_difficulty_tiebreaker(self):
        """Test qualification using difficulty as final tiebreaker."""
        users = [
            UserEntry(
                uid=f"uid-{i}",
                username=f"Player{i}",
                word_list=["A", "B", "C", "D", "E", "F"],
                score=50,  # All same score
                time=100,  # All same time
                timestamp="2025-01-27T12:00:00+00:00",
                word_list_difficulty=100.0 - i,  # Difficulties from 100 to 81
            )
            for i in range(20)
        ]
        
        lb = LeaderboardSettings(
            challenge_id="test",
            entry_type="daily",
            users=users,
        )
        
        # Same score/time but higher difficulty than lowest (81) should qualify
        assert check_qualification(lb, 50, 100, 82.0) is True
        # Lower difficulty should not qualify
        assert check_qualification(lb, 50, 100, 80.0) is False

    def test_not_qualify_lower_score(self):
        """Test that lower score doesn't qualify for full leaderboard."""
        users = [
            UserEntry(
                uid=f"uid-{i}",
                username=f"Player{i}",
                word_list=["A", "B", "C", "D", "E", "F"],
                score=100,  # All high scores
                time=60,
                timestamp="2025-01-27T12:00:00+00:00",
            )
            for i in range(20)
        ]
        
        lb = LeaderboardSettings(
            challenge_id="test",
            entry_type="daily",
            users=users,
        )
        
        # Much lower score should not qualify
        assert check_qualification(lb, 50, 60) is False


class TestDateIds:
    """Tests for date/week ID generation."""

    def test_daily_id_format(self):
        """Test daily ID format is YYYY-MM-DD."""
        daily_id = get_current_daily_id()
        # Should match pattern YYYY-MM-DD
        assert len(daily_id) == 10
        assert daily_id[4] == "-"
        assert daily_id[7] == "-"
        
        # Parse to verify it's a valid date
        date = datetime.strptime(daily_id, "%Y-%m-%d")
        assert date is not None

    def test_weekly_id_format(self):
        """Test weekly ID format is YYYY-Www."""
        weekly_id = get_current_weekly_id()
        # Should match pattern YYYY-Www
        assert "-W" in weekly_id
        parts = weekly_id.split("-W")
        assert len(parts) == 2
        assert len(parts[0]) == 4  # Year
        assert len(parts[1]) == 2  # Week number with leading zero

    def test_daily_path(self):
        """Test daily leaderboard path generation with new folder structure."""
        path = get_daily_leaderboard_path("2025-01-27", "classic-classic-0")
        assert path == "games/leaderboards/daily/2025-01-27/classic-classic-0/settings.json"

    def test_weekly_path(self):
        """Test weekly leaderboard path generation with new folder structure."""
        path = get_weekly_leaderboard_path("2025-W04", "easy-easy-1")
        assert path == "games/leaderboards/weekly/2025-W04/easy-easy-1/settings.json"


class TestSorting:
    """Tests for user sorting."""

    def test_sort_by_score_desc(self):
        """Test users are sorted by score descending."""
        users = [
            UserEntry(uid="1", username="A", word_list=[], score=30, time=100, timestamp="" ),
            UserEntry(uid="2", username="B", word_list=[], score=50, time=100, timestamp="" ),
            UserEntry(uid="3", username="C", word_list=[], score=40, time=100, timestamp="" ),
        ]
        
        sorted_users = _sort_users(users)
        
        assert sorted_users[0].score == 50
        assert sorted_users[1].score == 40
        assert sorted_users[2].score == 30

    def test_sort_by_time_asc_for_equal_score(self):
        """Test users with equal score are sorted by time ascending."""
        users = [
            UserEntry(uid="1", username="A", word_list=[], score=50, time=120, timestamp="" ),
            UserEntry(uid="2", username="B", word_list=[], score=50, time=80, timestamp="" ),
            UserEntry(uid="3", username="C", word_list=[], score=50, time=100, timestamp="" ),
        ]
        
        sorted_users = _sort_users(users)
        
        assert sorted_users[0].time == 80
        assert sorted_users[1].time == 100
        assert sorted_users[2].time == 120

    def test_sort_by_difficulty_desc_for_equal_score_and_time(self):
        """Test users with equal score and time are sorted by difficulty descending."""
        users = [
            UserEntry(uid="1", username="A", word_list=[], score=50, time=100, timestamp="", word_list_difficulty=80.0),
            UserEntry(uid="2", username="B", word_list=[], score=50, time=100, timestamp="", word_list_difficulty=120.0),
            UserEntry(uid="3", username="C", word_list=[], score=50, time=100, timestamp="", word_list_difficulty=100.0),
        ]
        
        sorted_users = _sort_users(users)
        
        assert sorted_users[0].word_list_difficulty == 120.0
        assert sorted_users[1].word_list_difficulty == 100.0
        assert sorted_users[2].word_list_difficulty == 80.0


class TestCreateUserEntry:
    """Tests for create_user_entry helper."""

    def test_create_user_entry_basic(self):
        """Test creating a user entry with basic fields."""
        entry = create_user_entry(
            username="TestPlayer",
            score=45,
            time_seconds=150,
            word_list=["A", "B", "C", "D", "E", "F"],
        )
        
        assert entry.username == "TestPlayer"
        assert entry.score == 45
        assert entry.time == 150
        assert len(entry.word_list) == 6
        assert entry.uid is not None  # Auto-generated
        assert entry.timestamp is not None  # Auto-generated

    def test_create_user_entry_with_optional_fields(self):
        """Test creating a user entry with optional fields."""
        entry = create_user_entry(
            username="TestPlayer",
            score=45,
            time_seconds=150,
            word_list=["A", "B", "C", "D", "E", "F"],
            word_list_difficulty=110.5,
            source_challenge_id="challenge-xyz",
        )
        
        assert entry.word_list_difficulty == 110.5
        assert entry.source_challenge_id == "challenge-xyz"


class TestUnifiedFormat:
    """Tests for unified format consistency."""

    def test_leaderboard_matches_challenge_structure(self):
        """Test leaderboard to_dict matches expected challenge structure."""
        lb = LeaderboardSettings(
            challenge_id="2025-01-27/classic-classic-0",
            entry_type="daily",
        )
        d = lb.to_dict()
        
        # All challenge fields should be present
        required_fields = [
            "challenge_id",
            "entry_type",
            "game_mode",
            "grid_size",
            "puzzle_options",
            "users",
            "created_at",
            "version",
            "show_incorrect_guesses",
            "enable_free_letters",
            "wordlist_source",
            "game_title",
            "max_display_entries",
        ]
        
        for field in required_fields:
            assert field in d, f"Missing field: {field}"

    def test_entry_type_field_present(self):
        """Test entry_type is always present in serialized output."""
        for entry_type in ["daily", "weekly", "challenge"]:
            lb = LeaderboardSettings(
                challenge_id="test",
                entry_type=entry_type,
            )
            d = lb.to_dict()
            assert d["entry_type"] == entry_type

    def test_challenge_id_as_primary_identifier(self):
        """Test challenge_id serves as primary identifier for all types."""
        # Daily uses new folder format
        daily = LeaderboardSettings(challenge_id="2025-01-27/classic-classic-0", entry_type="daily")
        assert daily.challenge_id == "2025-01-27/classic-classic-0"
        
        # Weekly uses new folder format
        weekly = LeaderboardSettings(challenge_id="2025-W04/easy-easy-0", entry_type="weekly")
        assert weekly.challenge_id == "2025-W04/easy-easy-0"
        
        # Challenge uses UID format
        challenge = LeaderboardSettings(challenge_id="20251130T190249Z-ABCDEF", entry_type="challenge")
        assert challenge.challenge_id == "20251130T190249Z-ABCDEF"