cricinfo-cli-go 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/AGENTS.md +63 -0
  2. package/CONTRIBUTORS.md +75 -0
  3. package/LICENSE +21 -0
  4. package/Makefile +131 -0
  5. package/README.md +130 -0
  6. package/bin/cricinfo.js +44 -0
  7. package/cmd/cricinfo/main.go +15 -0
  8. package/go.mod +10 -0
  9. package/go.sum +10 -0
  10. package/internal/app/app.go +11 -0
  11. package/internal/app/app_test.go +122 -0
  12. package/internal/buildinfo/buildinfo.go +16 -0
  13. package/internal/cli/analysis.go +262 -0
  14. package/internal/cli/analysis_test.go +175 -0
  15. package/internal/cli/competitions.go +154 -0
  16. package/internal/cli/competitions_test.go +165 -0
  17. package/internal/cli/leagues.go +297 -0
  18. package/internal/cli/leagues_test.go +194 -0
  19. package/internal/cli/matches.go +403 -0
  20. package/internal/cli/matches_test.go +413 -0
  21. package/internal/cli/players.go +263 -0
  22. package/internal/cli/players_test.go +384 -0
  23. package/internal/cli/root.go +141 -0
  24. package/internal/cli/search.go +119 -0
  25. package/internal/cli/teams.go +214 -0
  26. package/internal/cli/teams_test.go +192 -0
  27. package/internal/cricinfo/analysis.go +1401 -0
  28. package/internal/cricinfo/analysis_phase15_test.go +267 -0
  29. package/internal/cricinfo/client.go +471 -0
  30. package/internal/cricinfo/client_test.go +280 -0
  31. package/internal/cricinfo/cmd/fixture-refresh/main.go +145 -0
  32. package/internal/cricinfo/competitions.go +405 -0
  33. package/internal/cricinfo/competitions_phase13_test.go +234 -0
  34. package/internal/cricinfo/coverage_ledger.go +122 -0
  35. package/internal/cricinfo/coverage_ledger_test.go +253 -0
  36. package/internal/cricinfo/decode.go +115 -0
  37. package/internal/cricinfo/decode_test.go +100 -0
  38. package/internal/cricinfo/entity_index.go +618 -0
  39. package/internal/cricinfo/entity_index_test.go +175 -0
  40. package/internal/cricinfo/fixture_matrix.go +243 -0
  41. package/internal/cricinfo/fixture_matrix_test.go +49 -0
  42. package/internal/cricinfo/fixtures_test.go +264 -0
  43. package/internal/cricinfo/historical_hydration.go +1641 -0
  44. package/internal/cricinfo/historical_phase14_test.go +542 -0
  45. package/internal/cricinfo/leagues.go +1210 -0
  46. package/internal/cricinfo/leagues_phase12_test.go +324 -0
  47. package/internal/cricinfo/live_leagues_test.go +169 -0
  48. package/internal/cricinfo/live_matches_test.go +203 -0
  49. package/internal/cricinfo/live_matrix_test.go +118 -0
  50. package/internal/cricinfo/live_players_test.go +122 -0
  51. package/internal/cricinfo/live_search_test.go +86 -0
  52. package/internal/cricinfo/live_smoke_test.go +213 -0
  53. package/internal/cricinfo/live_teams_test.go +104 -0
  54. package/internal/cricinfo/matches.go +1508 -0
  55. package/internal/cricinfo/matches_phase7_test.go +207 -0
  56. package/internal/cricinfo/matches_phase9_test.go +253 -0
  57. package/internal/cricinfo/normalize_entities.go +1727 -0
  58. package/internal/cricinfo/normalize_leagues.go +346 -0
  59. package/internal/cricinfo/players.go +1332 -0
  60. package/internal/cricinfo/players_phase10_test.go +174 -0
  61. package/internal/cricinfo/players_phase11_test.go +373 -0
  62. package/internal/cricinfo/render_contract.go +1088 -0
  63. package/internal/cricinfo/render_phase4_test.go +633 -0
  64. package/internal/cricinfo/renderer.go +1689 -0
  65. package/internal/cricinfo/resolver.go +813 -0
  66. package/internal/cricinfo/resolver_test.go +244 -0
  67. package/internal/cricinfo/teams.go +603 -0
  68. package/internal/cricinfo/teams_phase8_test.go +231 -0
  69. package/internal/cricinfo/testdata/fixtures/README.md +43 -0
  70. package/internal/cricinfo/testdata/fixtures/aux-competition-metadata/broadcasts.json +11 -0
  71. package/internal/cricinfo/testdata/fixtures/aux-competition-metadata/officials.json +150 -0
  72. package/internal/cricinfo/testdata/fixtures/details-plays/detail-110.json +157 -0
  73. package/internal/cricinfo/testdata/fixtures/details-plays/detail-52545007.json +145 -0
  74. package/internal/cricinfo/testdata/fixtures/details-plays/detail-52559021.json +143 -0
  75. package/internal/cricinfo/testdata/fixtures/details-plays/plays.json +15 -0
  76. package/internal/cricinfo/testdata/fixtures/endpoint-matrix.tsv +19 -0
  77. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/fow-1.json +12 -0
  78. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/fow.json +42 -0
  79. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/innings-1-2.json +38 -0
  80. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/partnership-1.json +31 -0
  81. package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/partnerships.json +42 -0
  82. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar-offdays.json +20 -0
  83. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar-ondays.json +21 -0
  84. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar.json +14 -0
  85. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-2025.json +13 -0
  86. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-group-1.json +13 -0
  87. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-groups.json +11 -0
  88. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-type-1.json +13 -0
  89. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-types.json +11 -0
  90. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/seasons.json +30 -0
  91. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings-item-1.json +72 -0
  92. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings-root.json +3 -0
  93. package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings.json +15 -0
  94. package/internal/cricinfo/testdata/fixtures/matches-competitions/competition.json +460 -0
  95. package/internal/cricinfo/testdata/fixtures/matches-competitions/event-1529474.json +86 -0
  96. package/internal/cricinfo/testdata/fixtures/matches-competitions/matchcards-1527966.json +368 -0
  97. package/internal/cricinfo/testdata/fixtures/matches-competitions/situation-1529474.json +10 -0
  98. package/internal/cricinfo/testdata/fixtures/players/athlete-1361257-statistics.json +126 -0
  99. package/internal/cricinfo/testdata/fixtures/players/athlete-1361257.json +113 -0
  100. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores-1-1-statistics-0.json +208 -0
  101. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores-1-2-statistics-0.json +252 -0
  102. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores.json +74 -0
  103. package/internal/cricinfo/testdata/fixtures/players/roster-1361257-statistics-0.json +1008 -0
  104. package/internal/cricinfo/testdata/fixtures/root-discovery/events.json +72 -0
  105. package/internal/cricinfo/testdata/fixtures/root-discovery/root.json +28 -0
  106. package/internal/cricinfo/testdata/fixtures/team-competitor/competitor-789643.json +40 -0
  107. package/internal/cricinfo/testdata/fixtures/team-competitor/leaders-789643.json +353 -0
  108. package/internal/cricinfo/testdata/fixtures/team-competitor/records-789643.json +91 -0
  109. package/internal/cricinfo/testdata/fixtures/team-competitor/roster-1147772-object.json +231 -0
  110. package/internal/cricinfo/testdata/fixtures/team-competitor/roster-1147772.json +235 -0
  111. package/internal/cricinfo/testdata/fixtures/team-competitor/roster-789643.json +322 -0
  112. package/internal/cricinfo/testdata/fixtures/team-competitor/scores-789643.json +19 -0
  113. package/internal/cricinfo/testdata/fixtures/team-competitor/statistics-789643.json +629 -0
  114. package/internal/cricinfo/testdata/fixtures/team-competitor/team-789643-athletes.json +7 -0
  115. package/internal/cricinfo/testdata/fixtures/team-competitor/team-789643.json +67 -0
  116. package/internal/cricinfo/testdata/golden/match-empty.golden +1 -0
  117. package/internal/cricinfo/testdata/golden/match-list.golden +2 -0
  118. package/internal/cricinfo/testdata/golden/match-partial.golden +3 -0
  119. package/internal/cricinfo/types.go +54 -0
  120. package/package.json +51 -0
  121. package/scripts/postinstall.js +153 -0
@@ -0,0 +1,174 @@
1
+ package cricinfo
2
+
3
+ import (
4
+ "context"
5
+ "net/http"
6
+ "net/http/httptest"
7
+ "strings"
8
+ "testing"
9
+ "time"
10
+ )
11
+
12
+ func TestPlayerServicePhase10SearchProfileNewsAndStatistics(t *testing.T) {
13
+ t.Parallel()
14
+
15
+ athleteFixture := mustReadFixtureBytes(t, "players/athlete-1361257.json")
16
+ statisticsFixture := mustReadFixtureBytes(t, "players/athlete-1361257-statistics.json")
17
+
18
+ newsPage := []byte(`{"count":1,"pageIndex":1,"pageSize":25,"pageCount":1,"items":[{"$ref":"http://core.espnuk.org/v2/sports/cricket/news/1530499"}]}`)
19
+ newsItem := []byte(`{"$ref":"http://core.espnuk.org/v2/sports/cricket/news/1530499","id":"1530499","uid":"s:200~n:1530499","headline":"Virat Kohli news headline","title":"Virat Kohli news headline","linkText":"Virat Kohli news headline","byline":"ESPNcricinfo staff","description":"Story description","published":"2026-04-04T00:00Z","lastModified":"2026-04-05T11:08Z","links":{"api":{"v1":{"href":"http://api.espncricinfo.com/1/story/1530499"}},"web":{"href":"https://www.espn.in/ci/content/story/1530499.html"}}}`)
20
+
21
+ var server *httptest.Server
22
+ server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23
+ base := server.URL
24
+ switch r.URL.Path {
25
+ case "/v2/sports/cricket/events":
26
+ _, _ = w.Write([]byte(`{"count":0,"pageIndex":1,"pageSize":25,"pageCount":0,"items":[]}`))
27
+ case "/v2/sports/cricket/athletes/1361257", "/athletes/1361257":
28
+ _, _ = w.Write(rewriteFixtureBaseURL(athleteFixture, base))
29
+ case "/v2/sports/cricket/athletes/1361257/statistics", "/athletes/1361257/statistics":
30
+ _, _ = w.Write(rewriteFixtureBaseURL(statisticsFixture, base))
31
+ case "/v2/sports/cricket/athletes/1361257/news", "/athletes/1361257/news":
32
+ _, _ = w.Write(rewriteFixtureBaseURL(newsPage, base))
33
+ case "/v2/sports/cricket/news/1530499", "/news/1530499":
34
+ _, _ = w.Write(rewriteFixtureBaseURL(newsItem, base))
35
+ default:
36
+ http.NotFound(w, r)
37
+ }
38
+ }))
39
+ defer server.Close()
40
+
41
+ client, err := NewClient(Config{BaseURL: server.URL + "/v2/sports/cricket"})
42
+ if err != nil {
43
+ t.Fatalf("NewClient error: %v", err)
44
+ }
45
+
46
+ index, err := OpenEntityIndex(t.TempDir() + "/entity-index.json")
47
+ if err != nil {
48
+ t.Fatalf("OpenEntityIndex error: %v", err)
49
+ }
50
+ if err := index.Upsert(IndexedEntity{
51
+ Kind: EntityPlayer,
52
+ ID: "1361257",
53
+ Ref: server.URL + "/v2/sports/cricket/athletes/1361257",
54
+ Name: "Fazal Haq Shaheen",
55
+ ShortName: "Fazal Haq Shaheen",
56
+ Aliases: []string{"Fazal Haq Shaheen", "fazal haq shaheen", "1361257"},
57
+ }); err != nil {
58
+ t.Fatalf("index upsert error: %v", err)
59
+ }
60
+
61
+ resolver, err := NewResolver(ResolverConfig{
62
+ Client: client,
63
+ Index: index,
64
+ EventSeedTTL: 24 * time.Hour,
65
+ })
66
+ if err != nil {
67
+ t.Fatalf("NewResolver error: %v", err)
68
+ }
69
+
70
+ service, err := NewPlayerService(PlayerServiceConfig{
71
+ Client: client,
72
+ Resolver: resolver,
73
+ })
74
+ if err != nil {
75
+ t.Fatalf("NewPlayerService error: %v", err)
76
+ }
77
+
78
+ ctx := context.Background()
79
+
80
+ searchResult, err := service.Search(ctx, "Fazal Haq Shaheen", PlayerLookupOptions{Limit: 5})
81
+ if err != nil {
82
+ t.Fatalf("PlayerService.Search error: %v", err)
83
+ }
84
+ if len(searchResult.Items) == 0 {
85
+ t.Fatalf("expected search items")
86
+ }
87
+
88
+ profileResult, err := service.Profile(ctx, "Fazal Haq Shaheen", PlayerLookupOptions{})
89
+ if err != nil {
90
+ t.Fatalf("PlayerService.Profile error: %v", err)
91
+ }
92
+ profile, ok := profileResult.Data.(Player)
93
+ if !ok {
94
+ t.Fatalf("expected player profile data, got %T", profileResult.Data)
95
+ }
96
+ if profile.ID != "1361257" || profile.Team == nil {
97
+ t.Fatalf("expected normalized player profile with team, got %+v", profile)
98
+ }
99
+ if len(profile.Styles) == 0 || len(profile.MajorTeams) == 0 {
100
+ t.Fatalf("expected normalized styles and major teams, got %+v", profile)
101
+ }
102
+
103
+ statsResult, err := service.Stats(ctx, "Fazal Haq Shaheen", PlayerLookupOptions{})
104
+ if err != nil {
105
+ t.Fatalf("PlayerService.Stats error: %v", err)
106
+ }
107
+ stats, ok := statsResult.Data.(PlayerStatistics)
108
+ if !ok {
109
+ t.Fatalf("expected player statistics data, got %T", statsResult.Data)
110
+ }
111
+ if stats.PlayerID != "1361257" || len(stats.Categories) == 0 {
112
+ t.Fatalf("expected grouped player statistics, got %+v", stats)
113
+ }
114
+
115
+ newsResult, err := service.News(ctx, "Fazal Haq Shaheen", PlayerLookupOptions{Limit: 5})
116
+ if err != nil {
117
+ t.Fatalf("PlayerService.News error: %v", err)
118
+ }
119
+ if newsResult.Kind != EntityNewsArticle {
120
+ t.Fatalf("expected news result kind %q, got %q", EntityNewsArticle, newsResult.Kind)
121
+ }
122
+ if len(newsResult.Items) != 1 {
123
+ t.Fatalf("expected 1 news item, got %d (status=%s message=%q warnings=%v)", len(newsResult.Items), newsResult.Status, newsResult.Message, newsResult.Warnings)
124
+ }
125
+ article, ok := newsResult.Items[0].(NewsArticle)
126
+ if !ok {
127
+ t.Fatalf("expected normalized news article item, got %T", newsResult.Items[0])
128
+ }
129
+ if !strings.Contains(article.Headline, "Virat Kohli") {
130
+ t.Fatalf("expected normalized headline, got %+v", article)
131
+ }
132
+ }
133
+
134
+ func TestPlayerServicePhase10MissingPlayerReturnsEmptyResult(t *testing.T) {
135
+ t.Parallel()
136
+
137
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
138
+ switch r.URL.Path {
139
+ case "/v2/sports/cricket/events":
140
+ _, _ = w.Write([]byte(`{"count":0,"pageIndex":1,"pageSize":25,"pageCount":0,"items":[]}`))
141
+ default:
142
+ http.NotFound(w, r)
143
+ }
144
+ }))
145
+ defer server.Close()
146
+
147
+ index, err := OpenEntityIndex(t.TempDir() + "/entity-index.json")
148
+ if err != nil {
149
+ t.Fatalf("OpenEntityIndex error: %v", err)
150
+ }
151
+ client, err := NewClient(Config{BaseURL: server.URL + "/v2/sports/cricket"})
152
+ if err != nil {
153
+ t.Fatalf("NewClient error: %v", err)
154
+ }
155
+ resolver, err := NewResolver(ResolverConfig{Client: client, Index: index})
156
+ if err != nil {
157
+ t.Fatalf("NewResolver error: %v", err)
158
+ }
159
+ service, err := NewPlayerService(PlayerServiceConfig{Client: client, Resolver: resolver})
160
+ if err != nil {
161
+ t.Fatalf("NewPlayerService error: %v", err)
162
+ }
163
+
164
+ result, err := service.Profile(context.Background(), "missing player", PlayerLookupOptions{})
165
+ if err != nil {
166
+ t.Fatalf("PlayerService.Profile error: %v", err)
167
+ }
168
+ if result.Status != ResultStatusEmpty {
169
+ t.Fatalf("expected empty result, got %+v", result)
170
+ }
171
+ if !strings.Contains(result.Message, "no players found") {
172
+ t.Fatalf("expected missing player message, got %q", result.Message)
173
+ }
174
+ }
@@ -0,0 +1,373 @@
1
+ package cricinfo
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "net/http"
7
+ "net/http/httptest"
8
+ "path/filepath"
9
+ "strings"
10
+ "testing"
11
+ "time"
12
+ )
13
+
14
+ func TestPhase11FixtureNormalizationForPlayerBattingAndBowlingExtraction(t *testing.T) {
15
+ t.Parallel()
16
+
17
+ matchStatsBody := mustReadFixtureBytes(t, "players/roster-1361257-statistics-0.json")
18
+ matchCategories, err := NormalizeStatCategories(matchStatsBody)
19
+ if err != nil {
20
+ t.Fatalf("NormalizeStatCategories match stats fixture error: %v", err)
21
+ }
22
+
23
+ batting, bowling, _ := splitPlayerStatCategories(matchCategories)
24
+ if len(batting) == 0 {
25
+ t.Fatalf("expected batting categories from roster-player stats fixture")
26
+ }
27
+ if len(bowling) == 0 {
28
+ t.Fatalf("expected bowling categories from roster-player stats fixture")
29
+ }
30
+
31
+ summary := summarizePlayerMatchCategories(matchCategories)
32
+ if summary.BallsFaced == 0 {
33
+ t.Fatalf("expected ballsFaced summary from roster-player stats fixture, got %+v", summary)
34
+ }
35
+ if summary.StrikeRate == 0 {
36
+ t.Fatalf("expected strikeRate summary from roster-player stats fixture, got %+v", summary)
37
+ }
38
+ if strings.TrimSpace(summary.DismissalName) == "" || strings.TrimSpace(summary.DismissalCard) == "" {
39
+ t.Fatalf("expected dismissalName and dismissalCard in summary, got %+v", summary)
40
+ }
41
+ if summary.EconomyRate == 0 || summary.Maidens == 0 {
42
+ t.Fatalf("expected bowling rate/maidens summary fields, got %+v", summary)
43
+ }
44
+ if strings.TrimSpace(summary.BowlerPlayerID) == "" || strings.TrimSpace(summary.FielderPlayerID) == "" {
45
+ t.Fatalf("expected bowlerPlayerId and fielderPlayerId in summary, got %+v", summary)
46
+ }
47
+
48
+ inningsBattingBody := mustReadFixtureBytes(t, "players/roster-1361257-linescores-1-1-statistics-0.json")
49
+ inningsBattingCategories, err := NormalizeStatCategories(inningsBattingBody)
50
+ if err != nil {
51
+ t.Fatalf("NormalizeStatCategories innings batting fixture error: %v", err)
52
+ }
53
+ inningsBatting, inningsBowling, _ := splitPlayerStatCategories(inningsBattingCategories)
54
+ if len(inningsBatting) == 0 || len(inningsBowling) != 0 {
55
+ t.Fatalf("expected batting-only linescore split for period 1/1")
56
+ }
57
+
58
+ inningsBowlingBody := mustReadFixtureBytes(t, "players/roster-1361257-linescores-1-2-statistics-0.json")
59
+ inningsBowlingCategories, err := NormalizeStatCategories(inningsBowlingBody)
60
+ if err != nil {
61
+ t.Fatalf("NormalizeStatCategories innings bowling fixture error: %v", err)
62
+ }
63
+ inningsBatting2, inningsBowling2, _ := splitPlayerStatCategories(inningsBowlingCategories)
64
+ if len(inningsBatting2) != 0 || len(inningsBowling2) == 0 {
65
+ t.Fatalf("expected bowling-only linescore split for period 1/2")
66
+ }
67
+ }
68
+
69
+ func TestPhase11FixtureNormalizationPreservesDismissalAndDeliveryMetadata(t *testing.T) {
70
+ t.Parallel()
71
+
72
+ statsBody := mustReadFixtureBytes(t, "team-competitor/statistics-789643.json")
73
+ overs, wickets, err := NormalizeInningsPeriodStatistics(statsBody)
74
+ if err != nil {
75
+ t.Fatalf("NormalizeInningsPeriodStatistics fixture error: %v", err)
76
+ }
77
+ if len(overs) == 0 || len(wickets) == 0 {
78
+ t.Fatalf("expected over and wicket timelines from statistics fixture")
79
+ }
80
+ if wickets[0].DetailRef == "" || wickets[0].DismissalCard == "" {
81
+ t.Fatalf("expected wicket detailRef and dismissalCard in normalized wicket timeline, got %+v", wickets[0])
82
+ }
83
+ if wickets[0].StrikeRate == 0 {
84
+ t.Fatalf("expected wicket strikeRate in normalized wicket timeline, got %+v", wickets[0])
85
+ }
86
+
87
+ detailBody := mustReadFixtureBytes(t, "details-plays/detail-52559021.json")
88
+ delivery, err := NormalizeDeliveryEvent(detailBody)
89
+ if err != nil {
90
+ t.Fatalf("NormalizeDeliveryEvent dismissal fixture error: %v", err)
91
+ }
92
+ if strings.TrimSpace(delivery.BatsmanPlayerID) == "" {
93
+ t.Fatalf("expected batsmanPlayerId in normalized delivery, got %+v", delivery)
94
+ }
95
+ if strings.TrimSpace(delivery.BowlerPlayerID) == "" {
96
+ t.Fatalf("expected bowlerPlayerId in normalized delivery, got %+v", delivery)
97
+ }
98
+ if strings.TrimSpace(delivery.DismissalType) == "" || strings.TrimSpace(delivery.DismissalName) == "" {
99
+ t.Fatalf("expected dismissal type/name in normalized delivery, got %+v", delivery)
100
+ }
101
+ }
102
+
103
+ func TestPhase11DeliveryNormalizationHandlesStringAthleteRefs(t *testing.T) {
104
+ t.Parallel()
105
+
106
+ raw := []byte(`{
107
+ "$ref":"http://core.espnuk.org/v2/sports/cricket/leagues/11132/events/1527689/competitions/1527689/details/999",
108
+ "id":"999",
109
+ "team":"http://core.espnuk.org/v2/sports/cricket/teams/4340",
110
+ "batsman":{"athlete":"http://core.espnuk.org/v2/sports/cricket/athletes/253802"},
111
+ "bowler":{"athlete":"http://core.espnuk.org/v2/sports/cricket/athletes/26421"},
112
+ "dismissal":{"type":"caught","fielder":{"athlete":"http://core.espnuk.org/v2/sports/cricket/athletes/34102"}},
113
+ "athletesInvolved":[
114
+ "http://core.espnuk.org/v2/sports/cricket/athletes/253802",
115
+ {"$ref":"http://core.espnuk.org/v2/sports/cricket/athletes/26421"}
116
+ ]
117
+ }`)
118
+
119
+ delivery, err := NormalizeDeliveryEvent(raw)
120
+ if err != nil {
121
+ t.Fatalf("NormalizeDeliveryEvent string-ref payload error: %v", err)
122
+ }
123
+ if delivery.BatsmanPlayerID != "253802" {
124
+ t.Fatalf("expected batsman id 253802, got %+v", delivery)
125
+ }
126
+ if delivery.BowlerPlayerID != "26421" {
127
+ t.Fatalf("expected bowler id 26421, got %+v", delivery)
128
+ }
129
+ if delivery.FielderPlayerID != "34102" {
130
+ t.Fatalf("expected fielder id 34102, got %+v", delivery)
131
+ }
132
+ if len(delivery.AthletePlayerIDs) < 2 {
133
+ t.Fatalf("expected athlete ids extracted from mixed string/map payload, got %+v", delivery.AthletePlayerIDs)
134
+ }
135
+ }
136
+
137
+ func TestPlayerServicePhase11MatchContextCommands(t *testing.T) {
138
+ t.Parallel()
139
+
140
+ service := newPhase11PlayerTestService(t)
141
+
142
+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
143
+ defer cancel()
144
+
145
+ matchStatsResult, err := service.MatchStats(ctx, "Fazal Haq Shaheen", "1529474", PlayerLookupOptions{LeagueID: "19138"})
146
+ if err != nil {
147
+ t.Fatalf("PlayerService.MatchStats error: %v", err)
148
+ }
149
+ if matchStatsResult.Kind != EntityPlayerMatch {
150
+ t.Fatalf("expected kind %q, got %q", EntityPlayerMatch, matchStatsResult.Kind)
151
+ }
152
+ matchStats, ok := matchStatsResult.Data.(PlayerMatch)
153
+ if !ok {
154
+ t.Fatalf("expected PlayerMatch payload, got %T", matchStatsResult.Data)
155
+ }
156
+ if matchStats.PlayerID != "1361257" {
157
+ t.Fatalf("expected player id 1361257, got %+v", matchStats)
158
+ }
159
+ if matchStats.Summary.BallsFaced == 0 || matchStats.Summary.EconomyRate == 0 {
160
+ t.Fatalf("expected batting and bowling summary fields in match stats, got %+v", matchStats.Summary)
161
+ }
162
+
163
+ inningsResult, err := service.Innings(ctx, "Fazal Haq Shaheen", "1529474", PlayerLookupOptions{LeagueID: "19138"})
164
+ if err != nil {
165
+ t.Fatalf("PlayerService.Innings error: %v", err)
166
+ }
167
+ if inningsResult.Kind != EntityPlayerInnings {
168
+ t.Fatalf("expected kind %q, got %q", EntityPlayerInnings, inningsResult.Kind)
169
+ }
170
+ if len(inningsResult.Items) == 0 {
171
+ t.Fatalf("expected player innings entries")
172
+ }
173
+ firstInnings, ok := inningsResult.Items[0].(PlayerInnings)
174
+ if !ok {
175
+ t.Fatalf("expected PlayerInnings item, got %T", inningsResult.Items[0])
176
+ }
177
+ if strings.TrimSpace(firstInnings.StatisticsRef) == "" {
178
+ t.Fatalf("expected statistics ref in player innings entry")
179
+ }
180
+
181
+ deliveriesResult, err := service.Deliveries(ctx, "Fazal Haq Shaheen", "1529474", PlayerLookupOptions{LeagueID: "19138"})
182
+ if err != nil {
183
+ t.Fatalf("PlayerService.Deliveries error: %v", err)
184
+ }
185
+ if deliveriesResult.Kind != EntityPlayerDelivery {
186
+ t.Fatalf("expected kind %q, got %q", EntityPlayerDelivery, deliveriesResult.Kind)
187
+ }
188
+ if len(deliveriesResult.Items) == 0 {
189
+ t.Fatalf("expected delivery events for player")
190
+ }
191
+ foundDismissalDelivery := false
192
+ for _, raw := range deliveriesResult.Items {
193
+ delivery, ok := raw.(DeliveryEvent)
194
+ if !ok {
195
+ t.Fatalf("expected DeliveryEvent item, got %T", raw)
196
+ }
197
+ if strings.TrimSpace(delivery.BatsmanPlayerID) != "1361257" {
198
+ continue
199
+ }
200
+ if strings.Contains(delivery.Ref, "/details/52559021") {
201
+ foundDismissalDelivery = true
202
+ if strings.TrimSpace(delivery.DismissalType) == "" {
203
+ t.Fatalf("expected dismissal type for dismissal delivery, got %+v", delivery)
204
+ }
205
+ if len(delivery.Involvement) == 0 {
206
+ t.Fatalf("expected player involvement roles on delivery, got %+v", delivery)
207
+ }
208
+ }
209
+ }
210
+ if !foundDismissalDelivery {
211
+ t.Fatalf("expected dismissal delivery detail ref 52559021 in player deliveries output")
212
+ }
213
+
214
+ dismissalsResult, err := service.Dismissals(ctx, "Fazal Haq Shaheen", "1529474", PlayerLookupOptions{LeagueID: "19138"})
215
+ if err != nil {
216
+ t.Fatalf("PlayerService.Dismissals error: %v", err)
217
+ }
218
+ if dismissalsResult.Kind != EntityPlayerDismissal {
219
+ t.Fatalf("expected kind %q, got %q", EntityPlayerDismissal, dismissalsResult.Kind)
220
+ }
221
+ if len(dismissalsResult.Items) == 0 {
222
+ t.Fatalf("expected player dismissal entries")
223
+ }
224
+ firstDismissal, ok := dismissalsResult.Items[0].(PlayerDismissal)
225
+ if !ok {
226
+ t.Fatalf("expected PlayerDismissal item, got %T", dismissalsResult.Items[0])
227
+ }
228
+ if strings.TrimSpace(firstDismissal.DetailRef) == "" {
229
+ t.Fatalf("expected dismissal detailRef in player dismissal output")
230
+ }
231
+ if strings.TrimSpace(firstDismissal.DismissalCard) == "" {
232
+ t.Fatalf("expected dismissalCard in player dismissal output")
233
+ }
234
+ if strings.TrimSpace(firstDismissal.BowlerPlayerID) == "" {
235
+ t.Fatalf("expected bowlerPlayerId in player dismissal output")
236
+ }
237
+ }
238
+
239
+ func newPhase11PlayerTestService(t *testing.T) *PlayerService {
240
+ t.Helper()
241
+
242
+ competitionFixture := mustReadFixtureBytes(t, "matches-competitions/competition.json")
243
+ rosterFixture := mustReadFixtureBytes(t, "team-competitor/roster-789643.json")
244
+ playerStatsFixture := mustReadFixtureBytes(t, "players/roster-1361257-statistics-0.json")
245
+ playerLinescoresFixture := mustReadFixtureBytes(t, "players/roster-1361257-linescores.json")
246
+ playerLinescore11StatsFixture := mustReadFixtureBytes(t, "players/roster-1361257-linescores-1-1-statistics-0.json")
247
+ playerLinescore12StatsFixture := mustReadFixtureBytes(t, "players/roster-1361257-linescores-1-2-statistics-0.json")
248
+ detail110Fixture := mustReadFixtureBytes(t, "details-plays/detail-110.json")
249
+ detailDismissalFixture := mustReadFixtureBytes(t, "details-plays/detail-52559021.json")
250
+ inningsFixture := mustReadFixtureBytes(t, "innings-fow-partnerships/innings-1-2.json")
251
+ statisticsFixture := mustReadFixtureBytes(t, "team-competitor/statistics-789643.json")
252
+ teamFixture := mustReadFixtureBytes(t, "team-competitor/team-789643.json")
253
+
254
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
255
+ base := "http://" + r.Host + "/v2/sports/cricket"
256
+ competitionPath := "/v2/sports/cricket/leagues/19138/events/1529474/competitions/1529474"
257
+
258
+ switch {
259
+ case r.URL.Path == competitionPath:
260
+ competition := rewriteFixtureBaseURL(competitionFixture, base)
261
+ _, _ = w.Write(competition)
262
+ case r.URL.Path == "/v2/sports/cricket/teams/789643":
263
+ _, _ = w.Write(rewriteFixtureBaseURL(teamFixture, base))
264
+ case r.URL.Path == "/v2/sports/cricket/teams/789647":
265
+ _, _ = w.Write([]byte(`{"$ref":"` + base + `/teams/789647","id":"789647","displayName":"Speen Ghar Region","shortDisplayName":"SGH","abbreviation":"SGH"}`))
266
+ case strings.HasSuffix(r.URL.Path, "/competitors/789643/roster"):
267
+ _, _ = w.Write(rewriteFixtureBaseURL(rosterFixture, base))
268
+ case strings.HasSuffix(r.URL.Path, "/competitors/789643/roster/1361257/statistics/0"):
269
+ _, _ = w.Write(rewriteFixtureBaseURL(playerStatsFixture, base))
270
+ case strings.HasSuffix(r.URL.Path, "/competitors/789643/roster/1361257/linescores"):
271
+ _, _ = w.Write(rewriteFixtureBaseURL(playerLinescoresFixture, base))
272
+ case strings.HasSuffix(r.URL.Path, "/competitors/789643/roster/1361257/linescores/1/1/statistics/0"):
273
+ _, _ = w.Write(rewriteFixtureBaseURL(playerLinescore11StatsFixture, base))
274
+ case strings.HasSuffix(r.URL.Path, "/competitors/789643/roster/1361257/linescores/1/2/statistics/0"):
275
+ _, _ = w.Write(rewriteFixtureBaseURL(playerLinescore12StatsFixture, base))
276
+ case strings.HasSuffix(r.URL.Path, "/competitors/789643/roster/1361257/linescores/1/3/statistics/0"):
277
+ _, _ = w.Write(rewriteFixtureBaseURL(playerLinescore11StatsFixture, base))
278
+ case strings.HasSuffix(r.URL.Path, "/details"):
279
+ plays := fmt.Sprintf(`{"count":2,"pageIndex":1,"pageSize":25,"pageCount":1,"items":[{"$ref":"%s/leagues/19138/events/1529474/competitions/1529474/details/110"},{"$ref":"%s/leagues/19138/events/1529474/competitions/1529474/details/52559021"}]}`, base, base)
280
+ _, _ = w.Write([]byte(plays))
281
+ case strings.HasSuffix(r.URL.Path, "/details/110"):
282
+ _, _ = w.Write(rewriteFixtureBaseURL(detail110Fixture, base))
283
+ case strings.HasSuffix(r.URL.Path, "/details/52559021"):
284
+ _, _ = w.Write(rewriteFixtureBaseURL(detailDismissalFixture, base))
285
+ case strings.HasSuffix(r.URL.Path, "/competitors/789643/linescores"):
286
+ _, _ = w.Write([]byte(fmt.Sprintf(`{"count":1,"pageIndex":1,"pageSize":25,"pageCount":1,"items":[{"$ref":"%s/leagues/19138/events/1529474/competitions/1529474/competitors/789643/linescores/1/2"}]}`, base)))
287
+ case strings.HasSuffix(r.URL.Path, "/competitors/789643/linescores/1/2"):
288
+ _, _ = w.Write(rewriteFixtureBaseURL(inningsFixture, base))
289
+ case strings.HasSuffix(r.URL.Path, "/competitors/789643/linescores/1/2/statistics/0"):
290
+ _, _ = w.Write(rewriteFixtureBaseURL(statisticsFixture, base))
291
+ default:
292
+ http.NotFound(w, r)
293
+ }
294
+ }))
295
+ t.Cleanup(server.Close)
296
+
297
+ index, err := OpenEntityIndex(filepath.Join(t.TempDir(), "resolver-index.json"))
298
+ if err != nil {
299
+ t.Fatalf("OpenEntityIndex error: %v", err)
300
+ }
301
+ if err := index.UpsertMany([]IndexedEntity{
302
+ {
303
+ Kind: EntityMatch,
304
+ ID: "1529474",
305
+ Ref: "/leagues/19138/events/1529474/competitions/1529474",
306
+ Name: "3rd Match",
307
+ ShortName: "3rd Match",
308
+ LeagueID: "19138",
309
+ EventID: "1529474",
310
+ MatchID: "1529474",
311
+ Aliases: []string{"3rd Match", "1529474"},
312
+ UpdatedAt: time.Now().UTC(),
313
+ },
314
+ {
315
+ Kind: EntityPlayer,
316
+ ID: "1361257",
317
+ Ref: "/athletes/1361257",
318
+ Name: "Fazal Haq Shaheen",
319
+ ShortName: "Fazal Haq Shaheen",
320
+ LeagueID: "19138",
321
+ EventID: "1529474",
322
+ MatchID: "1529474",
323
+ Aliases: []string{"Fazal Haq Shaheen", "1361257"},
324
+ UpdatedAt: time.Now().UTC(),
325
+ },
326
+ {
327
+ Kind: EntityTeam,
328
+ ID: "789643",
329
+ Ref: "/teams/789643",
330
+ Name: "Boost Region",
331
+ ShortName: "BOOST",
332
+ LeagueID: "19138",
333
+ EventID: "1529474",
334
+ MatchID: "1529474",
335
+ Aliases: []string{"Boost Region", "BOOST", "789643"},
336
+ UpdatedAt: time.Now().UTC(),
337
+ },
338
+ }); err != nil {
339
+ t.Fatalf("index upsert error: %v", err)
340
+ }
341
+ index.SetLastEventsSeedAt(time.Now().UTC())
342
+
343
+ client, err := NewClient(Config{BaseURL: server.URL + "/v2/sports/cricket"})
344
+ if err != nil {
345
+ t.Fatalf("NewClient error: %v", err)
346
+ }
347
+
348
+ resolver, err := NewResolver(ResolverConfig{
349
+ Client: client,
350
+ Index: index,
351
+ EventSeedTTL: 24 * time.Hour,
352
+ Now: func() time.Time { return time.Now().UTC() },
353
+ })
354
+ if err != nil {
355
+ t.Fatalf("NewResolver error: %v", err)
356
+ }
357
+ t.Cleanup(func() {
358
+ _ = resolver.Close()
359
+ })
360
+
361
+ service, err := NewPlayerService(PlayerServiceConfig{
362
+ Client: client,
363
+ Resolver: resolver,
364
+ })
365
+ if err != nil {
366
+ t.Fatalf("NewPlayerService error: %v", err)
367
+ }
368
+ t.Cleanup(func() {
369
+ _ = service.Close()
370
+ })
371
+
372
+ return service
373
+ }