wsi_streamer/tile/
service.rs

1//! Tile Service for orchestrating tile generation.
2//!
3//! The TileService is the main entry point for tile requests. It orchestrates:
4//! - Request validation
5//! - Cache lookups
6//! - Slide access via registry
7//! - JPEG decoding and re-encoding
8//! - Result caching
9//!
10//! # Architecture
11//!
12//! ```text
13//! ┌─────────────────────────────────────────────────────────────────┐
14//! │                         TileService                              │
15//! │  ┌─────────────────────────────────────────────────────────┐    │
16//! │  │                    get_tile()                           │    │
17//! │  │  1. Validate params   4. Read tile from slide           │    │
18//! │  │  2. Check cache       5. Encode at quality              │    │
19//! │  │  3. Get slide         6. Cache & return                 │    │
20//! │  └─────────────────────────────────────────────────────────┘    │
21//! │           │                    │                    │            │
22//! │           ▼                    ▼                    ▼            │
23//! │    ┌───────────┐      ┌──────────────┐    ┌──────────────────┐  │
24//! │    │ TileCache │      │ SlideRegistry│    │ JpegTileEncoder  │  │
25//! │    └───────────┘      └──────────────┘    └──────────────────┘  │
26//! └─────────────────────────────────────────────────────────────────┘
27//! ```
28
29use std::sync::Arc;
30
31use bytes::Bytes;
32
33use crate::error::TileError;
34use crate::slide::{SlideRegistry, SlideSource};
35
36use super::cache::{TileCache, TileCacheKey};
37use super::encoder::{is_valid_quality, JpegTileEncoder, DEFAULT_JPEG_QUALITY};
38
39// =============================================================================
40// Tile Request
41// =============================================================================
42
43/// A request for a tile.
44///
45/// This struct contains all parameters needed to identify and render a tile.
46#[derive(Debug, Clone)]
47pub struct TileRequest {
48    /// Slide identifier (e.g., S3 path)
49    pub slide_id: String,
50
51    /// Pyramid level (0 = highest resolution)
52    pub level: usize,
53
54    /// Tile X coordinate (0-indexed from left)
55    pub tile_x: u32,
56
57    /// Tile Y coordinate (0-indexed from top)
58    pub tile_y: u32,
59
60    /// JPEG quality (1-100, defaults to 80)
61    pub quality: u8,
62}
63
64impl TileRequest {
65    /// Create a new tile request with default quality.
66    pub fn new(slide_id: impl Into<String>, level: usize, tile_x: u32, tile_y: u32) -> Self {
67        Self {
68            slide_id: slide_id.into(),
69            level,
70            tile_x,
71            tile_y,
72            quality: DEFAULT_JPEG_QUALITY,
73        }
74    }
75
76    /// Create a new tile request with specified quality.
77    pub fn with_quality(
78        slide_id: impl Into<String>,
79        level: usize,
80        tile_x: u32,
81        tile_y: u32,
82        quality: u8,
83    ) -> Self {
84        Self {
85            slide_id: slide_id.into(),
86            level,
87            tile_x,
88            tile_y,
89            quality,
90        }
91    }
92}
93
94// =============================================================================
95// Tile Response
96// =============================================================================
97
98/// Response from the tile service.
99#[derive(Debug, Clone)]
100pub struct TileResponse {
101    /// The encoded JPEG tile data
102    pub data: Bytes,
103
104    /// Whether this tile was served from cache
105    pub cache_hit: bool,
106
107    /// The JPEG quality used for encoding
108    pub quality: u8,
109}
110
111// =============================================================================
112// Tile Service
113// =============================================================================
114
115/// Service for generating and caching tiles.
116///
117/// The TileService orchestrates the full tile pipeline:
118/// 1. Validates request parameters
119/// 2. Checks the tile cache for existing results
120/// 3. Fetches the slide from the registry
121/// 4. Reads raw tile data from the slide
122/// 5. Decodes and re-encodes at the requested quality
123/// 6. Caches and returns the result
124///
125/// # Type Parameters
126///
127/// * `S` - The slide source type (e.g., S3-based source)
128///
129/// # Example
130///
131/// ```ignore
132/// use wsi_streamer::tile::{TileService, TileRequest};
133/// use wsi_streamer::slide::SlideRegistry;
134///
135/// // Create registry and service
136/// let registry = SlideRegistry::new(source);
137/// let service = TileService::new(registry);
138///
139/// // Request a tile
140/// let request = TileRequest::new("slides/sample.svs", 0, 1, 2);
141/// let response = service.get_tile(request).await?;
142///
143/// println!("Tile size: {} bytes, cache hit: {}", response.data.len(), response.cache_hit);
144/// ```
145pub struct TileService<S: SlideSource> {
146    /// The slide registry for accessing slides
147    registry: Arc<SlideRegistry<S>>,
148
149    /// Cache for encoded tiles
150    cache: TileCache,
151
152    /// JPEG encoder
153    encoder: JpegTileEncoder,
154}
155
156impl<S: SlideSource> TileService<S> {
157    /// Create a new tile service with default cache settings.
158    ///
159    /// Uses default tile cache capacity (100MB).
160    pub fn new(registry: SlideRegistry<S>) -> Self {
161        Self {
162            registry: Arc::new(registry),
163            cache: TileCache::new(),
164            encoder: JpegTileEncoder::new(),
165        }
166    }
167
168    /// Create a new tile service with a shared registry.
169    ///
170    /// This allows multiple services or components to share the same registry.
171    pub fn with_shared_registry(registry: Arc<SlideRegistry<S>>) -> Self {
172        Self {
173            registry,
174            cache: TileCache::new(),
175            encoder: JpegTileEncoder::new(),
176        }
177    }
178
179    /// Create a new tile service with custom cache capacity.
180    ///
181    /// # Arguments
182    ///
183    /// * `registry` - The slide registry
184    /// * `cache_capacity` - Maximum tile cache size in bytes
185    pub fn with_cache_capacity(registry: SlideRegistry<S>, cache_capacity: usize) -> Self {
186        Self {
187            registry: Arc::new(registry),
188            cache: TileCache::with_capacity(cache_capacity),
189            encoder: JpegTileEncoder::new(),
190        }
191    }
192
193    /// Get a tile, using cache when available.
194    ///
195    /// This is the main entry point for tile requests. It:
196    /// 1. Validates the request parameters
197    /// 2. Checks the cache for an existing tile
198    /// 3. If not cached, fetches from the slide and encodes
199    /// 4. Caches and returns the result
200    ///
201    /// # Errors
202    ///
203    /// Returns an error if:
204    /// - The slide cannot be found or opened
205    /// - The level is out of range
206    /// - The tile coordinates are out of bounds
207    /// - The tile data cannot be decoded or encoded
208    pub async fn get_tile(&self, request: TileRequest) -> Result<TileResponse, TileError> {
209        // Validate quality
210        if !is_valid_quality(request.quality) {
211            return Err(TileError::InvalidQuality {
212                quality: request.quality,
213            });
214        }
215        let quality = request.quality;
216
217        // Create cache key
218        let cache_key = TileCacheKey::new(
219            request.slide_id.as_str(),
220            request.level as u32,
221            request.tile_x,
222            request.tile_y,
223            quality,
224        );
225
226        // Check cache first
227        if let Some(cached_data) = self.cache.get(&cache_key).await {
228            return Ok(TileResponse {
229                data: cached_data,
230                cache_hit: true,
231                quality,
232            });
233        }
234
235        // Cache miss - need to generate tile
236        let tile_data = self.generate_tile(&request, quality).await?;
237
238        // Cache the result
239        self.cache.put(cache_key, tile_data.clone()).await;
240
241        Ok(TileResponse {
242            data: tile_data,
243            cache_hit: false,
244            quality,
245        })
246    }
247
248    /// Generate a tile without caching.
249    ///
250    /// This is useful for one-off requests or when you want to bypass the cache.
251    pub async fn generate_tile(
252        &self,
253        request: &TileRequest,
254        quality: u8,
255    ) -> Result<Bytes, TileError> {
256        // Get the slide from registry
257        let slide = self
258            .registry
259            .get_slide(&request.slide_id)
260            .await
261            .map_err(|e| match e {
262                crate::error::FormatError::Io(io_err) => {
263                    if matches!(io_err, crate::error::IoError::NotFound(_)) {
264                        TileError::SlideNotFound {
265                            slide_id: request.slide_id.clone(),
266                        }
267                    } else {
268                        TileError::Io(io_err)
269                    }
270                }
271                crate::error::FormatError::Tiff(tiff_err) => TileError::Slide(tiff_err),
272                crate::error::FormatError::UnsupportedFormat { reason } => {
273                    TileError::Slide(crate::error::TiffError::InvalidTagValue {
274                        tag: "Format",
275                        message: reason,
276                    })
277                }
278            })?;
279
280        // Validate level
281        let level_count = slide.level_count();
282        if request.level >= level_count {
283            return Err(TileError::InvalidLevel {
284                level: request.level,
285                max_levels: level_count,
286            });
287        }
288
289        // Validate tile coordinates
290        let (max_x, max_y) = slide
291            .tile_count(request.level)
292            .ok_or(TileError::InvalidLevel {
293                level: request.level,
294                max_levels: level_count,
295            })?;
296
297        if request.tile_x >= max_x || request.tile_y >= max_y {
298            return Err(TileError::TileOutOfBounds {
299                level: request.level,
300                x: request.tile_x,
301                y: request.tile_y,
302                max_x,
303                max_y,
304            });
305        }
306
307        // Read the raw tile data from the slide
308        let raw_tile = slide
309            .read_tile(request.level, request.tile_x, request.tile_y)
310            .await?;
311
312        // Decode and re-encode at the requested quality
313        let encoded_tile = self.encoder.encode(&raw_tile, quality)?;
314
315        Ok(encoded_tile)
316    }
317
318    /// Get tile cache statistics.
319    ///
320    /// Returns `(current_size, capacity, entry_count)`.
321    pub async fn cache_stats(&self) -> (usize, usize, usize) {
322        let size = self.cache.size().await;
323        let capacity = self.cache.capacity();
324        let count = self.cache.len().await;
325        (size, capacity, count)
326    }
327
328    /// Clear the tile cache.
329    pub async fn clear_cache(&self) {
330        self.cache.clear().await;
331    }
332
333    /// Invalidate cached tiles for a specific slide.
334    ///
335    /// This removes all cached tiles for the given slide from the tile cache.
336    /// Note: This is O(n) where n is the number of cached tiles.
337    pub async fn invalidate_slide(&self, _slide_id: &str) {
338        // TODO: Implement efficient per-slide invalidation
339        // For now, this would require iterating the cache which isn't supported
340        // by the LRU cache. A production implementation might use a different
341        // data structure or maintain a secondary index.
342    }
343
344    /// Get a reference to the underlying registry.
345    pub fn registry(&self) -> &Arc<SlideRegistry<S>> {
346        &self.registry
347    }
348
349    /// Generate a thumbnail for a slide.
350    ///
351    /// This finds the lowest resolution level that fits within the requested
352    /// max dimension and returns a tile or composited image.
353    ///
354    /// # Arguments
355    ///
356    /// * `slide_id` - The slide identifier
357    /// * `max_dimension` - Maximum width or height for the thumbnail
358    /// * `quality` - JPEG quality (1-100)
359    ///
360    /// # Returns
361    ///
362    /// A JPEG-encoded thumbnail image.
363    pub async fn generate_thumbnail(
364        &self,
365        slide_id: &str,
366        max_dimension: u32,
367        quality: u8,
368    ) -> Result<TileResponse, TileError> {
369        // Validate quality
370        if !is_valid_quality(quality) {
371            return Err(TileError::InvalidQuality { quality });
372        }
373
374        // Get the slide from registry
375        let slide = self
376            .registry
377            .get_slide(slide_id)
378            .await
379            .map_err(|e| match e {
380                crate::error::FormatError::Io(io_err) => {
381                    if matches!(io_err, crate::error::IoError::NotFound(_)) {
382                        TileError::SlideNotFound {
383                            slide_id: slide_id.to_string(),
384                        }
385                    } else {
386                        TileError::Io(io_err)
387                    }
388                }
389                crate::error::FormatError::Tiff(tiff_err) => TileError::Slide(tiff_err),
390                crate::error::FormatError::UnsupportedFormat { reason } => {
391                    TileError::Slide(crate::error::TiffError::InvalidTagValue {
392                        tag: "Format",
393                        message: reason,
394                    })
395                }
396            })?;
397
398        let (full_width, full_height) = slide.dimensions().ok_or(TileError::InvalidLevel {
399            level: 0,
400            max_levels: 0,
401        })?;
402
403        // Calculate target downsample
404        let max_dim = full_width.max(full_height);
405        let downsample = max_dim as f64 / max_dimension as f64;
406
407        // Find best level for this downsample (or use lowest resolution level)
408        let level = slide
409            .best_level_for_downsample(downsample)
410            .unwrap_or(slide.level_count().saturating_sub(1));
411
412        let info = slide.level_info(level).ok_or(TileError::InvalidLevel {
413            level,
414            max_levels: slide.level_count(),
415        })?;
416
417        // If single tile covers the entire level, just return that tile
418        if info.tiles_x == 1 && info.tiles_y == 1 {
419            let request = TileRequest::with_quality(slide_id, level, 0, 0, quality);
420            return self.get_tile(request).await;
421        }
422
423        // For multiple tiles, we need to composite them
424        // For now, return the first tile from the lowest resolution level
425        // as a simple implementation
426        let lowest_level = slide.level_count().saturating_sub(1);
427        let request = TileRequest::with_quality(slide_id, lowest_level, 0, 0, quality);
428        self.get_tile(request).await
429    }
430}
431
432// =============================================================================
433// Tests
434// =============================================================================
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439    use crate::error::IoError;
440    use crate::io::RangeReader;
441    use crate::slide::SlideSource;
442    use async_trait::async_trait;
443    use image::codecs::jpeg::JpegEncoder;
444    use image::{GrayImage, Luma};
445
446    /// Create a test JPEG image
447    fn create_test_jpeg() -> Vec<u8> {
448        let img = GrayImage::from_fn(256, 256, |x, y| {
449            let val = ((x + y) % 256) as u8;
450            Luma([val])
451        });
452
453        let mut buf = Vec::new();
454        let mut encoder = JpegEncoder::new_with_quality(&mut buf, 90);
455        encoder.encode_image(&img).unwrap();
456        buf
457    }
458
459    /// Create a minimal valid TIFF file with actual JPEG tile data
460    fn create_tiff_with_jpeg_tile() -> Vec<u8> {
461        let jpeg_data = create_test_jpeg();
462        let jpeg_len = jpeg_data.len() as u32;
463
464        // We need enough space for the TIFF structure + JPEG data
465        let tile_data_offset = 1000u32;
466        let total_size = tile_data_offset as usize + jpeg_data.len() + 100;
467        let mut data = vec![0u8; total_size];
468
469        // Little-endian TIFF header
470        data[0] = 0x49; // 'I'
471        data[1] = 0x49; // 'I'
472        data[2] = 0x2A; // Version 42
473        data[3] = 0x00;
474        data[4] = 0x08; // First IFD at offset 8
475        data[5] = 0x00;
476        data[6] = 0x00;
477        data[7] = 0x00;
478
479        // IFD at offset 8
480        // Entry count = 8
481        data[8] = 0x08;
482        data[9] = 0x00;
483
484        let mut offset = 10;
485
486        // Helper to write IFD entry
487        let write_entry =
488            |data: &mut [u8], offset: &mut usize, tag: u16, typ: u16, count: u32, value: u32| {
489                data[*offset..*offset + 2].copy_from_slice(&tag.to_le_bytes());
490                data[*offset + 2..*offset + 4].copy_from_slice(&typ.to_le_bytes());
491                data[*offset + 4..*offset + 8].copy_from_slice(&count.to_le_bytes());
492                data[*offset + 8..*offset + 12].copy_from_slice(&value.to_le_bytes());
493                *offset += 12;
494            };
495
496        // ImageWidth (2048)
497        write_entry(&mut data, &mut offset, 256, 4, 1, 2048);
498
499        // ImageLength (1536)
500        write_entry(&mut data, &mut offset, 257, 4, 1, 1536);
501
502        // Compression (7 = JPEG)
503        write_entry(&mut data, &mut offset, 259, 3, 1, 7);
504
505        // TileWidth (256)
506        write_entry(&mut data, &mut offset, 322, 3, 1, 256);
507
508        // TileLength (256)
509        write_entry(&mut data, &mut offset, 323, 3, 1, 256);
510
511        // TileOffsets - 8x6=48 tiles, all pointing to same JPEG data for simplicity
512        // Store offsets at position 200
513        write_entry(&mut data, &mut offset, 324, 4, 48, 200);
514
515        // TileByteCounts - all tiles have same size
516        write_entry(&mut data, &mut offset, 325, 4, 48, 600);
517
518        // BitsPerSample
519        write_entry(&mut data, &mut offset, 258, 3, 1, 8);
520
521        // Next IFD offset (0 = no more IFDs)
522        data[offset..offset + 4].copy_from_slice(&0u32.to_le_bytes());
523
524        // Write tile offsets array at offset 200 (all point to same tile data)
525        for i in 0..48u32 {
526            let arr_offset = 200 + (i as usize) * 4;
527            data[arr_offset..arr_offset + 4].copy_from_slice(&tile_data_offset.to_le_bytes());
528        }
529
530        // Write tile byte counts array at offset 600
531        for i in 0..48u32 {
532            let arr_offset = 600 + (i as usize) * 4;
533            data[arr_offset..arr_offset + 4].copy_from_slice(&jpeg_len.to_le_bytes());
534        }
535
536        // Write the actual JPEG tile data
537        data[tile_data_offset as usize..tile_data_offset as usize + jpeg_data.len()]
538            .copy_from_slice(&jpeg_data);
539
540        data
541    }
542
543    /// Mock range reader
544    struct MockReader {
545        data: Bytes,
546        identifier: String,
547    }
548
549    #[async_trait]
550    impl RangeReader for MockReader {
551        async fn read_exact_at(&self, offset: u64, len: usize) -> Result<Bytes, IoError> {
552            let start = offset as usize;
553            let end = start + len;
554            if end > self.data.len() {
555                return Err(IoError::RangeOutOfBounds {
556                    offset,
557                    requested: len as u64,
558                    size: self.data.len() as u64,
559                });
560            }
561            Ok(self.data.slice(start..end))
562        }
563
564        fn size(&self) -> u64 {
565            self.data.len() as u64
566        }
567
568        fn identifier(&self) -> &str {
569            &self.identifier
570        }
571    }
572
573    /// Mock slide source
574    struct MockSlideSource {
575        data: Bytes,
576    }
577
578    impl MockSlideSource {
579        fn new(data: Vec<u8>) -> Self {
580            Self {
581                data: Bytes::from(data),
582            }
583        }
584    }
585
586    #[async_trait]
587    impl SlideSource for MockSlideSource {
588        type Reader = MockReader;
589
590        async fn create_reader(&self, slide_id: &str) -> Result<Self::Reader, IoError> {
591            if slide_id.contains("notfound") {
592                return Err(IoError::NotFound(slide_id.to_string()));
593            }
594            Ok(MockReader {
595                data: self.data.clone(),
596                identifier: format!("mock://{}", slide_id),
597            })
598        }
599    }
600
601    #[tokio::test]
602    async fn test_tile_request_creation() {
603        let request = TileRequest::new("test.svs", 0, 1, 2);
604        assert_eq!(request.slide_id, "test.svs");
605        assert_eq!(request.level, 0);
606        assert_eq!(request.tile_x, 1);
607        assert_eq!(request.tile_y, 2);
608        assert_eq!(request.quality, DEFAULT_JPEG_QUALITY);
609
610        let request_q = TileRequest::with_quality("test.svs", 1, 3, 4, 95);
611        assert_eq!(request_q.quality, 95);
612    }
613
614    #[tokio::test]
615    async fn test_get_tile_success() {
616        let tiff_data = create_tiff_with_jpeg_tile();
617        let source = MockSlideSource::new(tiff_data);
618        let registry = SlideRegistry::new(source);
619        let service = TileService::new(registry);
620
621        let request = TileRequest::new("test.tif", 0, 0, 0);
622        let response = service.get_tile(request).await;
623
624        assert!(response.is_ok());
625        let response = response.unwrap();
626
627        // Should be a cache miss on first request
628        assert!(!response.cache_hit);
629        assert_eq!(response.quality, DEFAULT_JPEG_QUALITY);
630
631        // Verify it's valid JPEG
632        assert!(response.data.len() > 2);
633        assert_eq!(response.data[0], 0xFF);
634        assert_eq!(response.data[1], 0xD8);
635    }
636
637    #[tokio::test]
638    async fn test_get_tile_cache_hit() {
639        let tiff_data = create_tiff_with_jpeg_tile();
640        let source = MockSlideSource::new(tiff_data);
641        let registry = SlideRegistry::new(source);
642        let service = TileService::new(registry);
643
644        let request = TileRequest::new("test.tif", 0, 0, 0);
645
646        // First request - cache miss
647        let response1 = service.get_tile(request.clone()).await.unwrap();
648        assert!(!response1.cache_hit);
649
650        // Second request - cache hit
651        let response2 = service.get_tile(request).await.unwrap();
652        assert!(response2.cache_hit);
653        assert_eq!(response1.data, response2.data);
654    }
655
656    #[tokio::test]
657    async fn test_different_quality_different_cache() {
658        let tiff_data = create_tiff_with_jpeg_tile();
659        let source = MockSlideSource::new(tiff_data);
660        let registry = SlideRegistry::new(source);
661        let service = TileService::new(registry);
662
663        let request_q80 = TileRequest::with_quality("test.tif", 0, 0, 0, 80);
664        let request_q95 = TileRequest::with_quality("test.tif", 0, 0, 0, 95);
665
666        // Request at quality 80
667        let response1 = service.get_tile(request_q80.clone()).await.unwrap();
668        assert!(!response1.cache_hit);
669
670        // Request at quality 95 - should be cache miss (different quality)
671        let response2 = service.get_tile(request_q95).await.unwrap();
672        assert!(!response2.cache_hit);
673
674        // Request at quality 80 again - should be cache hit
675        let response3 = service.get_tile(request_q80).await.unwrap();
676        assert!(response3.cache_hit);
677    }
678
679    #[tokio::test]
680    async fn test_invalid_level() {
681        let tiff_data = create_tiff_with_jpeg_tile();
682        let source = MockSlideSource::new(tiff_data);
683        let registry = SlideRegistry::new(source);
684        let service = TileService::new(registry);
685
686        // Request level 5 when only level 0 exists
687        let request = TileRequest::new("test.tif", 5, 0, 0);
688        let result = service.get_tile(request).await;
689
690        assert!(result.is_err());
691        match result.unwrap_err() {
692            TileError::InvalidLevel { level, max_levels } => {
693                assert_eq!(level, 5);
694                assert_eq!(max_levels, 1);
695            }
696            e => panic!("Expected InvalidLevel error, got {:?}", e),
697        }
698    }
699
700    #[tokio::test]
701    async fn test_tile_out_of_bounds() {
702        let tiff_data = create_tiff_with_jpeg_tile();
703        let source = MockSlideSource::new(tiff_data);
704        let registry = SlideRegistry::new(source);
705        let service = TileService::new(registry);
706
707        // Request tile (100, 100) when max is (8, 6)
708        let request = TileRequest::new("test.tif", 0, 100, 100);
709        let result = service.get_tile(request).await;
710
711        assert!(result.is_err());
712        match result.unwrap_err() {
713            TileError::TileOutOfBounds {
714                level,
715                x,
716                y,
717                max_x,
718                max_y,
719            } => {
720                assert_eq!(level, 0);
721                assert_eq!(x, 100);
722                assert_eq!(y, 100);
723                assert_eq!(max_x, 8);
724                assert_eq!(max_y, 6);
725            }
726            e => panic!("Expected TileOutOfBounds error, got {:?}", e),
727        }
728    }
729
730    #[tokio::test]
731    async fn test_slide_not_found() {
732        let tiff_data = create_tiff_with_jpeg_tile();
733        let source = MockSlideSource::new(tiff_data);
734        let registry = SlideRegistry::new(source);
735        let service = TileService::new(registry);
736
737        let request = TileRequest::new("notfound.tif", 0, 0, 0);
738        let result = service.get_tile(request).await;
739
740        assert!(result.is_err());
741        match result.unwrap_err() {
742            TileError::SlideNotFound { slide_id } => {
743                assert_eq!(slide_id, "notfound.tif");
744            }
745            e => panic!("Expected SlideNotFound error, got {:?}", e),
746        }
747    }
748
749    #[tokio::test]
750    async fn test_cache_stats() {
751        let tiff_data = create_tiff_with_jpeg_tile();
752        let source = MockSlideSource::new(tiff_data);
753        let registry = SlideRegistry::new(source);
754        let service = TileService::with_cache_capacity(registry, 10 * 1024 * 1024); // 10MB
755
756        let (size, capacity, count) = service.cache_stats().await;
757        assert_eq!(size, 0);
758        assert_eq!(capacity, 10 * 1024 * 1024);
759        assert_eq!(count, 0);
760
761        // Add a tile
762        let request = TileRequest::new("test.tif", 0, 0, 0);
763        service.get_tile(request).await.unwrap();
764
765        let (size, _, count) = service.cache_stats().await;
766        assert!(size > 0);
767        assert_eq!(count, 1);
768    }
769
770    #[tokio::test]
771    async fn test_clear_cache() {
772        let tiff_data = create_tiff_with_jpeg_tile();
773        let source = MockSlideSource::new(tiff_data);
774        let registry = SlideRegistry::new(source);
775        let service = TileService::new(registry);
776
777        // Add some tiles
778        service
779            .get_tile(TileRequest::new("test.tif", 0, 0, 0))
780            .await
781            .unwrap();
782        service
783            .get_tile(TileRequest::new("test.tif", 0, 1, 0))
784            .await
785            .unwrap();
786
787        let (_, _, count) = service.cache_stats().await;
788        assert_eq!(count, 2);
789
790        // Clear cache
791        service.clear_cache().await;
792
793        let (size, _, count) = service.cache_stats().await;
794        assert_eq!(size, 0);
795        assert_eq!(count, 0);
796    }
797
798    #[tokio::test]
799    async fn test_quality_validation() {
800        let tiff_data = create_tiff_with_jpeg_tile();
801        let source = MockSlideSource::new(tiff_data);
802        let registry = SlideRegistry::new(source);
803        let service = TileService::new(registry);
804
805        // Quality 0 should be rejected
806        let request = TileRequest::with_quality("test.tif", 0, 0, 0, 0);
807        let result = service.get_tile(request).await;
808        assert!(matches!(
809            result,
810            Err(TileError::InvalidQuality { quality: 0 })
811        ));
812
813        // Quality 255 should be rejected
814        let request = TileRequest::with_quality("test.tif", 0, 1, 0, 255);
815        let result = service.get_tile(request).await;
816        assert!(matches!(
817            result,
818            Err(TileError::InvalidQuality { quality: 255 })
819        ));
820    }
821}