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,207 @@
1
+ package cricinfo
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "net/http"
7
+ "net/http/httptest"
8
+ "os"
9
+ "path/filepath"
10
+ "strings"
11
+ "testing"
12
+ "time"
13
+ )
14
+
15
+ func TestMatchServicePhase7DetailsAndPlaysRenderDeliveryEvents(t *testing.T) {
16
+ t.Parallel()
17
+
18
+ service := newPhase7TestService(t)
19
+
20
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
21
+ defer cancel()
22
+
23
+ detailsResult, err := service.Details(ctx, "3rd Match", MatchLookupOptions{LeagueID: "19138"})
24
+ if err != nil {
25
+ t.Fatalf("Details error: %v", err)
26
+ }
27
+ if detailsResult.Kind != EntityDeliveryEvent {
28
+ t.Fatalf("expected details kind %q, got %q", EntityDeliveryEvent, detailsResult.Kind)
29
+ }
30
+ if detailsResult.Status == ResultStatusError {
31
+ t.Fatalf("expected non-error details result, got %+v", detailsResult)
32
+ }
33
+ if !strings.Contains(detailsResult.RequestedRef, "/details") {
34
+ t.Fatalf("expected details requested ref to contain /details, got %q", detailsResult.RequestedRef)
35
+ }
36
+ if len(detailsResult.Items) == 0 {
37
+ t.Fatalf("expected detail items")
38
+ }
39
+ firstDetail, ok := detailsResult.Items[0].(DeliveryEvent)
40
+ if !ok {
41
+ t.Fatalf("expected first details item to be DeliveryEvent, got %T", detailsResult.Items[0])
42
+ }
43
+ if firstDetail.PlayType == nil {
44
+ t.Fatalf("expected playType in detail delivery event")
45
+ }
46
+ if firstDetail.Dismissal == nil {
47
+ t.Fatalf("expected dismissal in detail delivery event")
48
+ }
49
+
50
+ playsResult, err := service.Plays(ctx, "3rd Match", MatchLookupOptions{LeagueID: "19138"})
51
+ if err != nil {
52
+ t.Fatalf("Plays error: %v", err)
53
+ }
54
+ if playsResult.Kind != EntityDeliveryEvent {
55
+ t.Fatalf("expected plays kind %q, got %q", EntityDeliveryEvent, playsResult.Kind)
56
+ }
57
+ if !strings.Contains(playsResult.RequestedRef, "/plays") {
58
+ t.Fatalf("expected plays requested ref to contain /plays, got %q", playsResult.RequestedRef)
59
+ }
60
+ if len(playsResult.Items) == 0 {
61
+ t.Fatalf("expected play items")
62
+ }
63
+ }
64
+
65
+ func TestMatchServicePhase7ScorecardAndSituation(t *testing.T) {
66
+ t.Parallel()
67
+
68
+ service := newPhase7TestService(t)
69
+
70
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
71
+ defer cancel()
72
+
73
+ scorecardResult, err := service.Scorecard(ctx, "3rd Match", MatchLookupOptions{LeagueID: "19138"})
74
+ if err != nil {
75
+ t.Fatalf("Scorecard error: %v", err)
76
+ }
77
+ if scorecardResult.Kind != EntityMatchScorecard {
78
+ t.Fatalf("expected scorecard kind %q, got %q", EntityMatchScorecard, scorecardResult.Kind)
79
+ }
80
+ if scorecardResult.Status == ResultStatusError {
81
+ t.Fatalf("expected non-error scorecard result, got %+v", scorecardResult)
82
+ }
83
+ scorecard, ok := scorecardResult.Data.(*MatchScorecard)
84
+ if !ok {
85
+ t.Fatalf("expected scorecard data type *MatchScorecard, got %T", scorecardResult.Data)
86
+ }
87
+ if len(scorecard.BattingCards) == 0 {
88
+ t.Fatalf("expected batting cards in scorecard result")
89
+ }
90
+ if len(scorecard.BowlingCards) == 0 {
91
+ t.Fatalf("expected bowling cards in scorecard result")
92
+ }
93
+ if len(scorecard.PartnershipCards) == 0 {
94
+ t.Fatalf("expected partnerships cards in scorecard result")
95
+ }
96
+
97
+ situationResult, err := service.Situation(ctx, "3rd Match", MatchLookupOptions{LeagueID: "19138"})
98
+ if err != nil {
99
+ t.Fatalf("Situation error: %v", err)
100
+ }
101
+ if situationResult.Kind != EntityMatchSituation {
102
+ t.Fatalf("expected situation kind %q, got %q", EntityMatchSituation, situationResult.Kind)
103
+ }
104
+ if situationResult.Status != ResultStatusEmpty {
105
+ t.Fatalf("expected sparse situation to return empty status, got %q", situationResult.Status)
106
+ }
107
+ if strings.TrimSpace(situationResult.Message) == "" {
108
+ t.Fatalf("expected sparse situation message to be populated")
109
+ }
110
+ }
111
+
112
+ func newPhase7TestService(t *testing.T) *MatchService {
113
+ t.Helper()
114
+
115
+ playsFixture := mustReadFixtureBytes(t, "details-plays/plays.json")
116
+ detailFixture := mustReadFixtureBytes(t, "details-plays/detail-110.json")
117
+ scorecardFixture := mustReadFixtureBytes(t, "matches-competitions/matchcards-1527966.json")
118
+ situationFixture := mustReadFixtureBytes(t, "matches-competitions/situation-1529474.json")
119
+
120
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
121
+ base := "http://" + r.Host + "/v2/sports/cricket"
122
+ competitionPath := "/v2/sports/cricket/leagues/19138/events/1529474/competitions/1529474"
123
+
124
+ switch r.URL.Path {
125
+ case competitionPath:
126
+ competition := fmt.Sprintf(`{"$ref":"%s/leagues/19138/events/1529474/competitions/1529474","id":"1529474","description":"3rd Match","shortDescription":"3rd Match","date":"2026-04-09T05:30Z","details":{"$ref":"%s/leagues/19138/events/1529474/competitions/1529474/details"},"plays":null,"matchcards":{"$ref":"%s/leagues/19138/events/1529474/competitions/1529474/matchcards"},"situation":{"$ref":"%s/leagues/19138/events/1529474/competitions/1529474/situation"},"competitors":[]}`,
127
+ base, base, base, base)
128
+ _, _ = w.Write([]byte(competition))
129
+ case competitionPath + "/details":
130
+ _, _ = w.Write(rewriteFixtureBaseURL(playsFixture, base))
131
+ case competitionPath + "/plays":
132
+ _, _ = w.Write(rewriteFixtureBaseURL(playsFixture, base))
133
+ case competitionPath + "/details/110":
134
+ _, _ = w.Write(rewriteFixtureBaseURL(detailFixture, base))
135
+ case competitionPath + "/matchcards":
136
+ _, _ = w.Write(rewriteFixtureBaseURL(scorecardFixture, base))
137
+ case competitionPath + "/situation":
138
+ _, _ = w.Write(rewriteFixtureBaseURL(situationFixture, base))
139
+ default:
140
+ http.NotFound(w, r)
141
+ }
142
+ }))
143
+ t.Cleanup(server.Close)
144
+
145
+ index, err := OpenEntityIndex(filepath.Join(t.TempDir(), "resolver-index.json"))
146
+ if err != nil {
147
+ t.Fatalf("OpenEntityIndex error: %v", err)
148
+ }
149
+ if err := index.Upsert(IndexedEntity{
150
+ Kind: EntityMatch,
151
+ ID: "1529474",
152
+ Ref: "/leagues/19138/events/1529474/competitions/1529474",
153
+ Name: "3rd Match",
154
+ ShortName: "3rd Match",
155
+ LeagueID: "19138",
156
+ EventID: "1529474",
157
+ MatchID: "1529474",
158
+ Aliases: []string{"3rd Match", "match 1529474"},
159
+ UpdatedAt: time.Now().UTC(),
160
+ }); err != nil {
161
+ t.Fatalf("index upsert error: %v", err)
162
+ }
163
+ index.SetLastEventsSeedAt(time.Now().UTC())
164
+
165
+ client, err := NewClient(Config{BaseURL: server.URL + "/v2/sports/cricket"})
166
+ if err != nil {
167
+ t.Fatalf("NewClient error: %v", err)
168
+ }
169
+
170
+ resolver, err := NewResolver(ResolverConfig{
171
+ Client: client,
172
+ Index: index,
173
+ EventSeedTTL: 24 * time.Hour,
174
+ Now: func() time.Time { return time.Now().UTC() },
175
+ })
176
+ if err != nil {
177
+ t.Fatalf("NewResolver error: %v", err)
178
+ }
179
+ t.Cleanup(func() {
180
+ _ = resolver.Close()
181
+ })
182
+
183
+ service, err := NewMatchService(MatchServiceConfig{Client: client, Resolver: resolver})
184
+ if err != nil {
185
+ t.Fatalf("NewMatchService error: %v", err)
186
+ }
187
+ t.Cleanup(func() {
188
+ _ = service.Close()
189
+ })
190
+
191
+ return service
192
+ }
193
+
194
+ func mustReadFixtureBytes(t *testing.T, fixturePath string) []byte {
195
+ t.Helper()
196
+ path := filepath.Join("testdata", "fixtures", fixturePath)
197
+ data, err := os.ReadFile(path)
198
+ if err != nil {
199
+ t.Fatalf("read fixture %q: %v", path, err)
200
+ }
201
+ return data
202
+ }
203
+
204
+ func rewriteFixtureBaseURL(data []byte, baseURL string) []byte {
205
+ updated := strings.ReplaceAll(string(data), "http://core.espnuk.org/v2/sports/cricket", baseURL)
206
+ return []byte(updated)
207
+ }
@@ -0,0 +1,253 @@
1
+ package cricinfo
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "net/http"
7
+ "net/http/httptest"
8
+ "path/filepath"
9
+ "strconv"
10
+ "strings"
11
+ "testing"
12
+ "time"
13
+ )
14
+
15
+ func TestMatchServicePhase9InningsPartnershipsFOWAndDeliveries(t *testing.T) {
16
+ t.Parallel()
17
+
18
+ service := newPhase9MatchTestService(t)
19
+
20
+ ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
21
+ defer cancel()
22
+
23
+ inningsResult, err := service.Innings(ctx, "1529474", MatchInningsOptions{LeagueID: "19138", TeamQuery: "789643"})
24
+ if err != nil {
25
+ t.Fatalf("Innings error: %v", err)
26
+ }
27
+ if inningsResult.Kind != EntityInnings {
28
+ t.Fatalf("expected innings kind %q, got %q", EntityInnings, inningsResult.Kind)
29
+ }
30
+ if len(inningsResult.Items) == 0 {
31
+ t.Fatalf("expected innings items")
32
+ }
33
+ firstInnings, ok := inningsResult.Items[0].(Innings)
34
+ if !ok {
35
+ t.Fatalf("expected innings item type Innings, got %T", inningsResult.Items[0])
36
+ }
37
+ if firstInnings.TeamID != "789643" {
38
+ t.Fatalf("expected innings team id 789643, got %q", firstInnings.TeamID)
39
+ }
40
+ if len(firstInnings.OverTimeline) == 0 {
41
+ t.Fatalf("expected over timeline from period statistics")
42
+ }
43
+ if len(firstInnings.WicketTimeline) == 0 {
44
+ t.Fatalf("expected wicket timeline from period statistics")
45
+ }
46
+ if strings.TrimSpace(firstInnings.WicketTimeline[0].DetailRef) == "" {
47
+ t.Fatalf("expected wicket timeline detail ref link")
48
+ }
49
+
50
+ partnershipsResult, err := service.Partnerships(ctx, "1529474", MatchInningsOptions{LeagueID: "19138", TeamQuery: "789643", Innings: 1, Period: 2})
51
+ if err != nil {
52
+ t.Fatalf("Partnerships error: %v", err)
53
+ }
54
+ if partnershipsResult.Kind != EntityPartnership {
55
+ t.Fatalf("expected partnerships kind %q, got %q", EntityPartnership, partnershipsResult.Kind)
56
+ }
57
+ if len(partnershipsResult.Items) == 0 {
58
+ t.Fatalf("expected partnership items")
59
+ }
60
+ firstPartnership, ok := partnershipsResult.Items[0].(Partnership)
61
+ if !ok {
62
+ t.Fatalf("expected partnership item type Partnership, got %T", partnershipsResult.Items[0])
63
+ }
64
+ if firstPartnership.WicketNumber == 0 || firstPartnership.Runs == 0 {
65
+ t.Fatalf("expected detailed partnership payload, got %+v", firstPartnership)
66
+ }
67
+
68
+ fowResult, err := service.FallOfWicket(ctx, "1529474", MatchInningsOptions{LeagueID: "19138", TeamQuery: "789643", Innings: 1, Period: 2})
69
+ if err != nil {
70
+ t.Fatalf("FallOfWicket error: %v", err)
71
+ }
72
+ if fowResult.Kind != EntityFallOfWicket {
73
+ t.Fatalf("expected fow kind %q, got %q", EntityFallOfWicket, fowResult.Kind)
74
+ }
75
+ if len(fowResult.Items) == 0 {
76
+ t.Fatalf("expected fow items")
77
+ }
78
+ firstFOW, ok := fowResult.Items[0].(FallOfWicket)
79
+ if !ok {
80
+ t.Fatalf("expected fow item type FallOfWicket, got %T", fowResult.Items[0])
81
+ }
82
+ if firstFOW.WicketNumber == 0 || firstFOW.WicketOver == 0 {
83
+ t.Fatalf("expected detailed fow payload, got %+v", firstFOW)
84
+ }
85
+
86
+ deliveriesResult, err := service.Deliveries(ctx, "1529474", MatchInningsOptions{LeagueID: "19138", TeamQuery: "789643", Innings: 1, Period: 2})
87
+ if err != nil {
88
+ t.Fatalf("Deliveries error: %v", err)
89
+ }
90
+ if deliveriesResult.Kind != EntityInnings {
91
+ t.Fatalf("expected deliveries kind %q, got %q", EntityInnings, deliveriesResult.Kind)
92
+ }
93
+ deliveriesInnings, ok := deliveriesResult.Data.(Innings)
94
+ if !ok {
95
+ t.Fatalf("expected deliveries data Innings, got %T", deliveriesResult.Data)
96
+ }
97
+ if len(deliveriesInnings.OverTimeline) == 0 || len(deliveriesInnings.WicketTimeline) == 0 {
98
+ t.Fatalf("expected deliveries timelines in innings payload, got %+v", deliveriesInnings)
99
+ }
100
+ }
101
+
102
+ func TestMatchServicePhase9MissingInningsOrPeriodShowsAvailableHint(t *testing.T) {
103
+ t.Parallel()
104
+
105
+ service := newPhase9MatchTestService(t)
106
+
107
+ ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
108
+ defer cancel()
109
+
110
+ result, err := service.Partnerships(ctx, "1529474", MatchInningsOptions{LeagueID: "19138", TeamQuery: "789643", Innings: 9, Period: 9})
111
+ if err != nil {
112
+ t.Fatalf("Partnerships error: %v", err)
113
+ }
114
+ if result.Status != ResultStatusEmpty {
115
+ t.Fatalf("expected empty result for missing innings/period, got status=%q", result.Status)
116
+ }
117
+ if !strings.Contains(result.Message, "available") || !strings.Contains(result.Message, "1/2") {
118
+ t.Fatalf("expected available innings/period hint, got %q", result.Message)
119
+ }
120
+ }
121
+
122
+ func newPhase9MatchTestService(t *testing.T) *MatchService {
123
+ t.Helper()
124
+
125
+ competitionFixture := mustReadFixtureBytes(t, "matches-competitions/competition.json")
126
+ teamFixture := mustReadFixtureBytes(t, "team-competitor/team-789643.json")
127
+ inningsFixture := mustReadFixtureBytes(t, "innings-fow-partnerships/innings-1-2.json")
128
+ partnershipsFixture := mustReadFixtureBytes(t, "innings-fow-partnerships/partnerships.json")
129
+ partnershipFixture := mustReadFixtureBytes(t, "innings-fow-partnerships/partnership-1.json")
130
+ fowFixture := mustReadFixtureBytes(t, "innings-fow-partnerships/fow.json")
131
+ fowItemFixture := mustReadFixtureBytes(t, "innings-fow-partnerships/fow-1.json")
132
+ statisticsFixture := mustReadFixtureBytes(t, "team-competitor/statistics-789643.json")
133
+
134
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
135
+ base := "http://" + r.Host + "/v2/sports/cricket"
136
+ competitionPath := "/v2/sports/cricket/leagues/19138/events/1529474/competitions/1529474"
137
+
138
+ switch {
139
+ case r.URL.Path == competitionPath:
140
+ _, _ = w.Write(rewriteFixtureBaseURL(competitionFixture, base))
141
+ case r.URL.Path == "/v2/sports/cricket/teams/789643":
142
+ _, _ = w.Write(rewriteFixtureBaseURL(teamFixture, base))
143
+ case r.URL.Path == "/v2/sports/cricket/teams/789647":
144
+ _, _ = w.Write([]byte(`{"$ref":"` + base + `/teams/789647","id":"789647","displayName":"Speen Ghar Region","shortDisplayName":"SGH","abbreviation":"SGH"}`))
145
+ case strings.HasSuffix(r.URL.Path, "/competitors/789643/linescores/1"):
146
+ payload := fmt.Sprintf(`{"count":1,"pageIndex":1,"pageSize":25,"pageCount":1,"items":[%s]}`,
147
+ string(rewriteFixtureBaseURL(inningsFixture, base)),
148
+ )
149
+ _, _ = w.Write([]byte(payload))
150
+ case strings.HasSuffix(r.URL.Path, "/competitors/789643/linescores"):
151
+ payload := fmt.Sprintf(`{"count":1,"pageIndex":1,"pageSize":25,"pageCount":1,"items":[{"$ref":"%s/leagues/19138/events/1529474/competitions/1529474/competitors/789643/linescores/1/2"}]}`,
152
+ base,
153
+ )
154
+ _, _ = w.Write([]byte(payload))
155
+ case strings.HasSuffix(r.URL.Path, "/competitors/789643/linescores/1/2"):
156
+ _, _ = w.Write(rewriteFixtureBaseURL(inningsFixture, base))
157
+ case strings.HasSuffix(r.URL.Path, "/competitors/789643/linescores/1/2/statistics/0"):
158
+ _, _ = w.Write(rewriteFixtureBaseURL(statisticsFixture, base))
159
+ case strings.HasSuffix(r.URL.Path, "/linescores/1/2/partnerships"):
160
+ _, _ = w.Write(rewriteFixtureBaseURL(partnershipsFixture, base))
161
+ case strings.Contains(r.URL.Path, "/linescores/1/2/partnerships/"):
162
+ partnershipID := lastPathComponent(r.URL.Path)
163
+ item := strings.ReplaceAll(string(rewriteFixtureBaseURL(partnershipFixture, base)), "/partnerships/1\"", "/partnerships/"+partnershipID+"\"")
164
+ item = strings.ReplaceAll(item, `"wicketNumber": 1`, `"wicketNumber": `+partnershipID)
165
+ _, _ = w.Write([]byte(item))
166
+ case strings.HasSuffix(r.URL.Path, "/linescores/1/2/fow"):
167
+ _, _ = w.Write(rewriteFixtureBaseURL(fowFixture, base))
168
+ case strings.Contains(r.URL.Path, "/linescores/1/2/fow/"):
169
+ wicketID := lastPathComponent(r.URL.Path)
170
+ item := strings.ReplaceAll(string(rewriteFixtureBaseURL(fowItemFixture, base)), "/fow/1\"", "/fow/"+wicketID+"\"")
171
+ item = strings.ReplaceAll(item, `"wicketNumber": 1`, `"wicketNumber": `+wicketID)
172
+ _, _ = w.Write([]byte(item))
173
+ default:
174
+ http.NotFound(w, r)
175
+ }
176
+ }))
177
+ t.Cleanup(server.Close)
178
+
179
+ index, err := OpenEntityIndex(filepath.Join(t.TempDir(), "resolver-index.json"))
180
+ if err != nil {
181
+ t.Fatalf("OpenEntityIndex error: %v", err)
182
+ }
183
+ if err := index.UpsertMany([]IndexedEntity{
184
+ {
185
+ Kind: EntityMatch,
186
+ ID: "1529474",
187
+ Ref: "/leagues/19138/events/1529474/competitions/1529474",
188
+ Name: "3rd Match",
189
+ ShortName: "3rd Match",
190
+ LeagueID: "19138",
191
+ EventID: "1529474",
192
+ MatchID: "1529474",
193
+ Aliases: []string{"3rd Match", "1529474"},
194
+ UpdatedAt: time.Now().UTC(),
195
+ },
196
+ {
197
+ Kind: EntityTeam,
198
+ ID: "789643",
199
+ Ref: "/teams/789643",
200
+ Name: "Boost Region",
201
+ ShortName: "BOOST",
202
+ LeagueID: "19138",
203
+ EventID: "1529474",
204
+ MatchID: "1529474",
205
+ Aliases: []string{"Boost Region", "BOOST", "789643"},
206
+ UpdatedAt: time.Now().UTC(),
207
+ },
208
+ }); err != nil {
209
+ t.Fatalf("index upsert error: %v", err)
210
+ }
211
+ index.SetLastEventsSeedAt(time.Now().UTC())
212
+
213
+ client, err := NewClient(Config{BaseURL: server.URL + "/v2/sports/cricket"})
214
+ if err != nil {
215
+ t.Fatalf("NewClient error: %v", err)
216
+ }
217
+
218
+ resolver, err := NewResolver(ResolverConfig{
219
+ Client: client,
220
+ Index: index,
221
+ EventSeedTTL: 24 * time.Hour,
222
+ Now: func() time.Time { return time.Now().UTC() },
223
+ })
224
+ if err != nil {
225
+ t.Fatalf("NewResolver error: %v", err)
226
+ }
227
+ t.Cleanup(func() {
228
+ _ = resolver.Close()
229
+ })
230
+
231
+ service, err := NewMatchService(MatchServiceConfig{Client: client, Resolver: resolver})
232
+ if err != nil {
233
+ t.Fatalf("NewMatchService error: %v", err)
234
+ }
235
+ t.Cleanup(func() {
236
+ _ = service.Close()
237
+ })
238
+
239
+ return service
240
+ }
241
+
242
+ func lastPathComponent(path string) string {
243
+ trimmed := strings.Trim(strings.TrimSpace(path), "/")
244
+ parts := strings.Split(trimmed, "/")
245
+ if len(parts) == 0 {
246
+ return "1"
247
+ }
248
+ last := strings.TrimSpace(parts[len(parts)-1])
249
+ if _, err := strconv.Atoi(last); err != nil {
250
+ return "1"
251
+ }
252
+ return last
253
+ }