# Refactor: route ServeTile through ITileService

**Task**: AZ-310_refactor_servetile_via_tileservice
**Name**: ServeTile via ITileService.GetOrDownloadTileAsync
**Description**: Move tile cache+DB+download logic from the `/tiles/{z}/{x}/{y}` endpoint into `ITileService` and make the endpoint a thin handler.
**Complexity**: 3 points
**Dependencies**: None
**Component**: TileDownloader (currently in SatelliteProvider.Services)
**Tracker**: AZ-310
**Epic**: AZ-309

## Problem

`Program.cs:141` (`ServeTile`) directly injects `ISatelliteDownloader`, `ITileRepository`, and `IMemoryCache`. It re-implements cache-or-DB-or-download logic that overlaps with `TileService` but is not delegated. This is architecture baseline finding F3 (Medium).

## Outcome

- All tile-serving logic for `/tiles/{z}/{x}/{y}` is owned by `TileService`.
- `ServeTile` handler is a thin function: validate route, call service, write headers, return bytes.
- `IMemoryCache` is no longer injected at the endpoint level for tile serving.

## Scope

### Included
- New method `Task<TileBytes> GetOrDownloadTileAsync(int z, int x, int y, CancellationToken ct = default)` on `ITileService`. `TileBytes` is a record `(byte[] Bytes, string ContentType, string ETag, TimeSpan MaxAge)`.
- Implementation in `TileService` owns the in-memory cache (DI-injected).
- New unit tests for `GetOrDownloadTileAsync` (cache hit, repo hit, downloader fallback).
- `Program.cs` ServeTile handler thinned.

### Excluded
- Adding a new integration test for `/tiles/{z}/{x}/{y}` (existing smoke does not cover it; out of scope here).
- Renaming any database tables, columns, or DTOs.

## Acceptance Criteria

**AC-1: Endpoint route preserved**
Given the API is running
When a client calls `GET /tiles/{z}/{x}/{y}`
Then the response shape (image/jpeg bytes, ETag header, Cache-Control header) is unchanged from before the refactor.

**AC-2: Cache hit path**
Given a tile has previously been served and is still in the in-memory cache
When `GetOrDownloadTileAsync` is called for the same `(z,x,y)`
Then the result comes from cache and neither the repository nor the downloader is invoked.

**AC-3: Repo hit path**
Given a tile is not in cache but exists in the database with a valid file path
When `GetOrDownloadTileAsync` is called
Then the file is read from disk, cached, and returned without invoking the downloader.

**AC-4: Downloader fallback path**
Given a tile is neither in cache nor in the database
When `GetOrDownloadTileAsync` is called
Then `ISatelliteDownloader.DownloadSingleTileAsync` is invoked, the downloaded tile is inserted into the repository, the bytes are read, cached, and returned.

**AC-5: Endpoint no longer injects downloader/repo/cache**
Given the post-refactor `Program.cs`
When the `ServeTile` handler is inspected
Then it injects only `ITileService`, `HttpContext`, and `ILogger<Program>` (not `ISatelliteDownloader`, `ITileRepository`, or `IMemoryCache`).

## Unit Tests

| AC Ref | What to Test | Required Outcome |
|--------|--------------|------------------|
| AC-2 | Cache hit | Mock cache returns bytes; repo + downloader mocks asserted unused |
| AC-3 | Repo hit | Mock repo returns tile entity with existing file path; downloader mock asserted unused |
| AC-4 | Downloader fallback | Mock repo returns null; downloader returns DownloadedTileInfoV2; assert insert + return |

## Constraints

- Public route, query/body shape, response shape preserved.
- No new external libraries.
- Cache lifetime (1h absolute, 30min sliding) preserved exactly.

## Risks & Mitigation

**Risk 1: Cache singleton lifetime semantics change**
- *Risk*: Moving `IMemoryCache` from endpoint scope into service scope might alter cache key collision behavior.
- *Mitigation*: TileService is registered as `Singleton`; `IMemoryCache` is also Singleton. Cache keys remain `tile_{z}_{x}_{y}`.
