using FluentAssertions; using SatelliteProvider.Common.Utils; namespace SatelliteProvider.Tests; public class TileCsvWriterTests : IDisposable { private readonly string _tempDir; public TileCsvWriterTests() { _tempDir = Path.Combine(Path.GetTempPath(), $"tilecsv_tests_{Guid.NewGuid():N}"); Directory.CreateDirectory(_tempDir); } public void Dispose() { if (Directory.Exists(_tempDir)) { Directory.Delete(_tempDir, recursive: true); } } [Fact] public async Task WriteAsync_WritesHeaderAndRows_AZ368_AC1() { // Arrange var sut = new TileCsvWriter(); var path = Path.Combine(_tempDir, "tiles.csv"); var rows = new[] { new TileCsvRow(47.461747, 37.647063, "/tiles/a.jpg"), new TileCsvRow(47.460000, 37.648000, "/tiles/b.jpg"), }; // Act await sut.WriteAsync(path, rows); // Assert var lines = await File.ReadAllLinesAsync(path); lines.Should().HaveCount(3); lines[0].Should().Be("latitude,longitude,file_path"); lines.Skip(1).Should().Contain("47.461747,37.647063,/tiles/a.jpg"); lines.Skip(1).Should().Contain("47.460000,37.648000,/tiles/b.jpg"); } [Fact] public async Task WriteAsync_OrdersByLatitudeDescThenLongitudeAsc_AZ368() { // Arrange var sut = new TileCsvWriter(); var path = Path.Combine(_tempDir, "ordered.csv"); // Intentionally unsorted; expected order is lat DESC, then lon ASC. var rows = new[] { new TileCsvRow(10.0, 30.0, "/c.jpg"), new TileCsvRow(20.0, 20.0, "/b.jpg"), new TileCsvRow(20.0, 10.0, "/a.jpg"), }; // Act await sut.WriteAsync(path, rows); // Assert var lines = await File.ReadAllLinesAsync(path); lines[0].Should().Be("latitude,longitude,file_path"); lines[1].Should().Be("20.000000,10.000000,/a.jpg", "lat=20 wins over lat=10; lon=10 wins over lon=20"); lines[2].Should().Be("20.000000,20.000000,/b.jpg"); lines[3].Should().Be("10.000000,30.000000,/c.jpg", "lat=10 comes last in desc order"); } [Fact] public async Task WriteAsync_EmptyRows_WritesHeaderOnly_AZ368() { // Arrange var sut = new TileCsvWriter(); var path = Path.Combine(_tempDir, "empty.csv"); // Act await sut.WriteAsync(path, Array.Empty()); // Assert var lines = await File.ReadAllLinesAsync(path); lines.Should().ContainSingle(); lines[0].Should().Be("latitude,longitude,file_path"); } [Fact] public async Task WriteAsync_FormatsNumericValuesWithF6Precision_AZ368_AC2() { // Arrange var sut = new TileCsvWriter(); var path = Path.Combine(_tempDir, "format.csv"); var rows = new[] { new TileCsvRow(1.0, 2.0, "/x.jpg"), new TileCsvRow(0.1234567890, 99.0000001, "/y.jpg"), }; // Act await sut.WriteAsync(path, rows); // Assert — F6 trims/pads to exactly 6 decimal places var lines = await File.ReadAllLinesAsync(path); lines.Should().Contain("1.000000,2.000000,/x.jpg"); lines.Should().Contain("0.123457,99.000000,/y.jpg"); } [Fact] public async Task WriteAsync_ProducesByteForByteOutputMatchingOldInlineWriter_AZ368_AC2() { // Arrange — replicate the exact pre-refactor inline writer behavior to // prove the extracted class doesn't drift the on-disk format. var rows = new[] { new TileCsvRow(47.461747, 37.647063, "/tiles/a.jpg"), new TileCsvRow(47.460000, 37.648000, "/tiles/b.jpg"), new TileCsvRow(47.470000, 37.640000, "/tiles/c.jpg"), }; var newPath = Path.Combine(_tempDir, "new.csv"); var oldPath = Path.Combine(_tempDir, "old.csv"); // Act — new path via TileCsvWriter await new TileCsvWriter().WriteAsync(newPath, rows); // Act — old path via inlined original logic var ordered = rows.OrderByDescending(t => t.Latitude).ThenBy(t => t.Longitude).ToList(); using (var writer = new StreamWriter(oldPath)) { await writer.WriteLineAsync("latitude,longitude,file_path"); foreach (var t in ordered) { await writer.WriteLineAsync($"{t.Latitude:F6},{t.Longitude:F6},{t.FilePath}"); } } // Assert var newBytes = await File.ReadAllBytesAsync(newPath); var oldBytes = await File.ReadAllBytesAsync(oldPath); newBytes.Should().Equal(oldBytes, "AZ-368 AC-2 requires byte-for-byte identical CSV output"); } [Fact] public async Task WriteAsync_NullPath_ThrowsArgumentNullException_AZ368() { // Arrange var sut = new TileCsvWriter(); // Act Func act = () => sut.WriteAsync(null!, Array.Empty()); // Assert await act.Should().ThrowAsync(); } [Fact] public async Task WriteAsync_NullRows_ThrowsArgumentNullException_AZ368() { // Arrange var sut = new TileCsvWriter(); var path = Path.Combine(_tempDir, "nullrows.csv"); // Act Func act = () => sut.WriteAsync(path, null!); // Assert await act.Should().ThrowAsync(); } }