1package services
2
3import (
4 "context"
5 "database/sql"
6 "testing"
7 "time"
8
9 "github.com/bnema/dumber/internal/config"
10 "github.com/bnema/dumber/internal/db"
11 mock_db "github.com/bnema/dumber/internal/db/mocks"
12 "go.uber.org/mock/gomock"
13)
14
15// TestBrowserService_FuzzyCacheIntegration tests the fuzzy cache lifecycle in BrowserService
16func TestBrowserService_FuzzyCacheIntegration(t *testing.T) {
17 ctrl := gomock.NewController(t)
18 defer ctrl.Finish()
19
20 mockDB := mock_db.NewMockDatabaseQuerier(ctrl)
21 ctx := context.Background()
22
23 // Mock test history data
24 testHistory := []db.History{
25 {
26 ID: 1,
27 Url: "https://github.com",
28 Title: sql.NullString{
29 String: "GitHub",
30 Valid: true,
31 },
32 VisitCount: sql.NullInt64{Int64: 5, Valid: true},
33 },
34 {
35 ID: 2,
36 Url: "https://google.com",
37 Title: sql.NullString{
38 String: "Google",
39 Valid: true,
40 },
41 VisitCount: sql.NullInt64{Int64: 10, Valid: true},
42 },
43 {
44 ID: 3,
45 Url: "https://stackoverflow.com",
46 Title: sql.NullString{
47 String: "Stack Overflow",
48 Valid: true,
49 },
50 VisitCount: sql.NullInt64{Int64: 3, Valid: true},
51 },
52 }
53
54 // Expect GetHistory to be called when loading fuzzy cache
55 mockDB.EXPECT().
56 GetHistory(gomock.Any(), gomock.Any()).
57 Return(testHistory, nil).
58 MinTimes(1)
59
60 // Create browser service (fuzzy cache initialized)
61 cfg := &config.Config{}
62 service := NewBrowserService(cfg, mockDB)
63
64 // Test 1: Verify fuzzy cache was initialized
65 if service.fuzzyCache == nil {
66 t.Fatal("Fuzzy cache should be initialized")
67 }
68
69 // Test 2: Load fuzzy cache from DB
70 err := service.LoadFuzzyCacheFromDB(ctx)
71 if err != nil {
72 t.Fatalf("Failed to load fuzzy cache: %v", err)
73 }
74
75 // Test 3: Verify cache has entries
76 stats := service.fuzzyCache.Stats()
77 if stats.EntryCount == 0 {
78 t.Error("Fuzzy cache should have entries after loading")
79 }
80
81 t.Logf("Fuzzy cache loaded: %d entries, %d trigrams", stats.EntryCount, stats.TrigramCount)
82
83 // Test 4: Verify refresh tracking is initialized
84 if service.historySinceRefresh != 0 {
85 t.Error("historySinceRefresh should start at 0")
86 }
87
88 if service.lastCacheRefresh.IsZero() {
89 t.Error("lastCacheRefresh should be initialized")
90 }
91
92 // Test 5: Flush all caches (should include fuzzy cache)
93 err = service.FlushAllCaches(ctx)
94 if err != nil {
95 t.Fatalf("Failed to flush caches: %v", err)
96 }
97
98 t.Log("Fuzzy cache integration test passed")
99}
100
101// TestBrowserService_FuzzyCacheSmartRefresh tests the smart refresh logic
102func TestBrowserService_FuzzyCacheSmartRefresh(t *testing.T) {
103 ctrl := gomock.NewController(t)
104 defer ctrl.Finish()
105
106 mockDB := mock_db.NewMockDatabaseQuerier(ctrl)
107 cfg := &config.Config{}
108
109 // Expect GetHistory to be called when loading fuzzy cache
110 mockDB.EXPECT().
111 GetHistory(gomock.Any(), gomock.Any()).
112 Return([]db.History{}, nil).
113 AnyTimes()
114
115 service := NewBrowserService(cfg, mockDB)
116
117 // Load initial cache
118 ctx := context.Background()
119 err := service.LoadFuzzyCacheFromDB(ctx)
120 if err != nil {
121 t.Fatalf("Failed to load fuzzy cache: %v", err)
122 }
123
124 // Test 1: Verify initial state
125 if service.historySinceRefresh != 0 {
126 t.Error("historySinceRefresh should start at 0")
127 }
128
129 initialRefreshTime := service.lastCacheRefresh
130
131 // Test 2: Simulate adding less than threshold entries (< 10)
132 service.historySinceRefresh = 5
133 service.lastCacheRefresh = time.Now().Add(-10 * time.Minute) // Old enough
134
135 // This should NOT trigger refresh (not enough entries)
136 // Manually check the condition
137 const minEntries = 10
138 const minInterval = 5 * time.Minute
139
140 shouldRefresh := service.historySinceRefresh >= minEntries &&
141 time.Since(service.lastCacheRefresh) >= minInterval
142
143 if shouldRefresh {
144 t.Error("Should not trigger refresh with only 5 entries")
145 }
146
147 // Test 3: Simulate enough entries but too recent
148 service.historySinceRefresh = 15
149 service.lastCacheRefresh = time.Now() // Just refreshed
150
151 shouldRefresh = service.historySinceRefresh >= minEntries &&
152 time.Since(service.lastCacheRefresh) >= minInterval
153
154 if shouldRefresh {
155 t.Error("Should not trigger refresh when last refresh was too recent")
156 }
157
158 // Test 4: Simulate conditions that SHOULD trigger refresh
159 service.historySinceRefresh = 15
160 service.lastCacheRefresh = time.Now().Add(-10 * time.Minute)
161
162 shouldRefresh = service.historySinceRefresh >= minEntries &&
163 time.Since(service.lastCacheRefresh) >= minInterval
164
165 if !shouldRefresh {
166 t.Error("Should trigger refresh with 15 entries and 10 minutes passed")
167 }
168
169 t.Logf("Smart refresh logic validated: %d entries, %v since refresh, should refresh: %v",
170 service.historySinceRefresh,
171 time.Since(service.lastCacheRefresh),
172 shouldRefresh)
173
174 // Test 5: Verify counter would reset after refresh
175 if shouldRefresh {
176 // Simulate the reset that happens in the actual code
177 service.historySinceRefresh = 0
178 service.lastCacheRefresh = time.Now()
179
180 if service.historySinceRefresh != 0 {
181 t.Error("Counter should reset after refresh")
182 }
183 }
184
185 // Test 6: Verify old refresh time was updated
186 if !service.lastCacheRefresh.After(initialRefreshTime) {
187 t.Error("Refresh time should be updated")
188 }
189
190 t.Log("Smart refresh logic test passed")
191}
192
193// TestBrowserService_FuzzyCacheThresholds tests the threshold constants
194func TestBrowserService_FuzzyCacheThresholds(t *testing.T) {
195 // This test documents and validates the threshold values
196
197 const minEntries = 10 // From the actual code
198 const minInterval = 5 * time.Minute // From the actual code
199
200 t.Logf("Fuzzy cache refresh thresholds:")
201 t.Logf(" - Minimum entries: %d", minEntries)
202 t.Logf(" - Minimum interval: %v", minInterval)
203
204 // Test reasonable threshold values
205 if minEntries < 5 {
206 t.Error("minEntries too low - will refresh too frequently")
207 }
208
209 if minEntries > 100 {
210 t.Error("minEntries too high - cache will be stale")
211 }
212
213 if minInterval < 1*time.Minute {
214 t.Error("minInterval too low - will refresh too frequently")
215 }
216
217 if minInterval > 30*time.Minute {
218 t.Error("minInterval too high - cache will be stale")
219 }
220
221 // Test that both conditions are required
222 // This prevents:
223 // - Refreshing after 1 entry but 10 minutes (would be wasteful)
224 // - Refreshing after 100 entries but 1 second (would be too frequent)
225
226 t.Log("Threshold validation passed - both conditions required for refresh")
227}
228
229// TestBrowserService_FuzzyCacheLoadFromDB tests loading cache at startup
230func TestBrowserService_FuzzyCacheLoadFromDB(t *testing.T) {
231 ctrl := gomock.NewController(t)
232 defer ctrl.Finish()
233
234 mockDB := mock_db.NewMockDatabaseQuerier(ctrl)
235 ctx := context.Background()
236
237 // Mock test history entries
238 testHistory := []db.History{
239 {
240 ID: 1,
241 Url: "https://github.com/user/repo",
242 Title: sql.NullString{String: "GitHub Repository", Valid: true},
243 VisitCount: sql.NullInt64{Int64: 10, Valid: true},
244 },
245 {
246 ID: 2,
247 Url: "https://stackoverflow.com/questions/123",
248 Title: sql.NullString{String: "Stack Overflow Question", Valid: true},
249 VisitCount: sql.NullInt64{Int64: 5, Valid: true},
250 },
251 {
252 ID: 3,
253 Url: "https://golang.org/doc",
254 Title: sql.NullString{String: "Go Documentation", Valid: true},
255 VisitCount: sql.NullInt64{Int64: 15, Valid: true},
256 },
257 {
258 ID: 4,
259 Url: "https://reddit.com/r/golang",
260 Title: sql.NullString{String: "Golang Subreddit", Valid: true},
261 VisitCount: sql.NullInt64{Int64: 3, Valid: true},
262 },
263 {
264 ID: 5,
265 Url: "https://news.ycombinator.com",
266 Title: sql.NullString{String: "Hacker News", Valid: true},
267 VisitCount: sql.NullInt64{Int64: 20, Valid: true},
268 },
269 }
270
271 // Expect GetHistory to be called when loading fuzzy cache
272 mockDB.EXPECT().
273 GetHistory(gomock.Any(), gomock.Any()).
274 Return(testHistory, nil).
275 MinTimes(1)
276
277 // Create service and load cache
278 cfg := &config.Config{}
279 service := NewBrowserService(cfg, mockDB)
280
281 // Load cache from DB
282 err := service.LoadFuzzyCacheFromDB(ctx)
283 if err != nil {
284 t.Fatalf("Failed to load fuzzy cache from DB: %v", err)
285 }
286
287 // Verify cache has loaded entries
288 stats := service.fuzzyCache.Stats()
289 if stats.EntryCount < len(testHistory) {
290 t.Errorf("Expected at least %d entries, got %d", len(testHistory), stats.EntryCount)
291 }
292
293 if stats.TrigramCount == 0 {
294 t.Error("Expected trigram index to be built")
295 }
296
297 t.Logf("Loaded fuzzy cache: %d entries, %d trigrams", stats.EntryCount, stats.TrigramCount)
298
299 // Test search functionality
300 result, err := service.fuzzyCache.Search(ctx, "github")
301 if err != nil {
302 t.Fatalf("Failed to search fuzzy cache: %v", err)
303 }
304
305 if len(result.Matches) == 0 {
306 t.Error("Expected to find matches for 'github'")
307 }
308
309 t.Logf("Search for 'github' found %d matches", len(result.Matches))
310
311 t.Log("Load from DB test passed")
312}