using TourMonitor.Notifications; namespace TourMonitor.Tests; public class ReportBuilderTests { private static ReportLine Line( int hotelId = 9099454, string dateFrom = "2026-08-20", int price = 475_447, string meal = "AI", string roomName = "Полулюкс", string operatorName = "Level.Travel", string link = "/package_details/offer-1", int? previous = null, int? historicalMin = null) => new(hotelId, dateFrom, price, meal, roomName, operatorName, link, previous, historicalMin); [Fact] public void BuildHotelSection_GroupsByMonth_ShowsTopPricesPerMonth() { var lines = new[] { Line(dateFrom: "2026-08-30", price: 520_000), Line(dateFrom: "2026-08-20", price: 475_447), Line(dateFrom: "2026-09-02", price: 501_000), Line(dateFrom: "2026-09-03", price: 490_000), }; var section = ReportBuilder.BuildHotelSection("Sunrise Tucana Resort", 2, lines); Assert.NotNull(section); Assert.Contains("Sunrise Tucana Resort", section); Assert.Contains("Август", section); Assert.Contains("Сентябрь", section); Assert.Contains("20.08", section); Assert.Contains("30.08", section); Assert.Contains("03.09", section); Assert.Contains("02.09", section); var months = section.Split("\n📅 "); Assert.Equal(3, months.Length); // отель + Август + Сентябрь Assert.Equal(2, months[1].Split("\n 📅 ").Length - 1); // Август — 2 дешёвых Assert.Equal(2, months[2].Split("\n 📅 ").Length - 1); // Сентябрь — 2 дешёвых } [Fact] public void BuildHotelSection_TakesNoMoreThanPricesPerMonth_WithinEachMonth() { var lines = new[] { Line(dateFrom: "2026-08-10", price: 500_000), Line(dateFrom: "2026-08-11", price: 510_000), Line(dateFrom: "2026-08-12", price: 520_000), Line(dateFrom: "2026-08-13", price: 530_000), }; var section = ReportBuilder.BuildHotelSection("Sunrise Tucana Resort", 2, lines); Assert.NotNull(section); Assert.Contains("Август", section); Assert.Contains("10.08", section); Assert.Contains("11.08", section); Assert.DoesNotContain("12.08", section); Assert.DoesNotContain("13.08", section); } [Fact] public void DescribeDelta_ShowsDrop_WithPercentage() { var line = Line(price: 475_447, previous: 540_000); var section = ReportBuilder.BuildHotelSection("Sunrise Tucana Resort", 5, new[] { line }); Assert.Contains("🔻 −12% (было 540\u00A0000 ₽)", section); } [Fact] public void DescribeDelta_ShowsNoChange_WhenPricesEqual() { var line = Line(price: 475_447, previous: 475_447); var section = ReportBuilder.BuildHotelSection("Sunrise Tucana Resort", 5, new[] { line }); Assert.Contains("= (без изменений)", section); } [Fact] public void DescribeDelta_MarksNewMinimum() { var line = Line(price: 410_000, previous: 420_000, historicalMin: 415_000); var section = ReportBuilder.BuildHotelSection("Sunrise Tucana Resort", 5, new[] { line }); Assert.Contains("✨ новая минималка", section); } [Fact] public void DescribeDelta_OmitsDelta_WhenNoPrevious() { var line = Line(previous: null); var section = ReportBuilder.BuildHotelSection("Sunrise Tucana Resort", 5, new[] { line }); Assert.DoesNotContain("было", section); Assert.DoesNotContain("🔻", section); Assert.DoesNotContain("🔺", section); } [Fact] public void BuildHotelSection_ReturnsNull_WhenNoData() { Assert.Null(ReportBuilder.BuildHotelSection("Sunrise Tucana Resort", 5, Array.Empty())); } [Fact] public void Build_IncludesHeader_AndOnlyNonEmptySections() { var sections = ReportBuilder.Build( "📊 Мониторинг туров · 9 ночей, 2+1 чел", 5, new[] { (9099454, "Sunrise Tucana Resort"), (9151153, "Posh Club By Sunrise Tucana Resort") }, new[] { Line(hotelId: 9099454) }); Assert.NotNull(sections); Assert.Contains("Мониторинг туров", sections); Assert.Contains("Sunrise Tucana Resort", sections); Assert.DoesNotContain("Posh Club", sections); } [Fact] public void Build_ReturnsNull_WhenNoLines() { var result = ReportBuilder.Build( "header", 5, new[] { (9099454, "Sunrise Tucana Resort") }, Array.Empty()); Assert.Null(result); } [Fact] public void Html_SpecialCharacters_AreEscaped() { var line = Line(roomName: "Стандарт x", operatorName: "ANEX & Co"); var section = ReportBuilder.BuildHotelSection("Sunrise Tucana", 5, new[] { line }); Assert.Contains("Sunrise <i>Tucana</i>", section); Assert.Contains("Стандарт <b>x</b>", section); Assert.Contains("ANEX & Co", section); } [Fact] public void RoomName_IsLinked_InsteadOfReserveLink() { var line = Line(link: "/package_details/offer-1?hotel_id=9099454", roomName: "Делюкс с видом на сад"); var section = ReportBuilder.BuildHotelSection("Sunrise Tucana Resort", 5, new[] { line }); Assert.Contains("https://level.travel/package_details/offer-1?hotel_id=9099454", section); Assert.Contains("Делюкс с видом на сад", section); Assert.DoesNotContain("забронировать", section); Assert.DoesNotContain("🔗", section); } [Fact] public void RoomName_IsNotLinked_WhenLinkEmpty() { var line = Line(link: "", roomName: "Делюкс с видом на сад"); var section = ReportBuilder.BuildHotelSection("Sunrise Tucana Resort", 5, new[] { line }); Assert.Contains("🍽 AI · Делюкс с видом на сад · Level.Travel", section); Assert.DoesNotContain("Мониторинг туров", 10, new[] { (9099454, "Sunrise Tucana Resort"), (9151153, "Posh Club By Sunrise Tucana Resort") }, new[] { Line(hotelId: 9099454, price: 400000), Line(hotelId: 9151153, price: 500000), }); Assert.NotNull(messages); Assert.Equal(2, messages.Count); Assert.Contains("Мониторинг туров", messages[0]); Assert.Contains("Sunrise Tucana Resort", messages[0]); Assert.DoesNotContain("Posh Club", messages[0]); Assert.Contains("Posh Club By Sunrise Tucana Resort", messages[1]); Assert.DoesNotContain("Мониторинг туров", messages[1]); } [Fact] public void BuildMessages_ReturnsNull_WhenNoLines() { var messages = ReportBuilder.BuildMessages( "header", 10, new[] { (9099454, "Sunrise Tucana Resort") }, Array.Empty()); Assert.Null(messages); } [Fact] public void BuildHotelSection_WithCalendarUrl_AddsCalendarLink_RightAfterHotelName() { var section = ReportBuilder.BuildHotelSection( "Sunrise Tucana Resort", 5, new[] { Line(price: 400000) }, calendarUrl: "http://192.168.1.6:8084/prices/9099454"); Assert.Contains("календарь цен", section); Assert.Contains("http://192.168.1.6:8084/prices/9099454", section); Assert.Contains("Sunrise Tucana Resort\n🗓 ")); } [Fact] public void BuildHotelSection_WithoutCalendarUrl_HasNoCalendarLink() { var section = ReportBuilder.BuildHotelSection("Sunrise Tucana Resort", 5, new[] { Line(price: 400000) }); Assert.DoesNotContain("календарь цен", section); } [Fact] public void SplitMessage_ChunksOversizedTextAtLineBoundaries() { var lines = Enumerable.Range(0, 50).Select(i => $"📅 10.10 — 475 447 ₽\n 🍽 AI · Полулюкс · Level.Travel").ToList(); var text = string.Join("\n", lines); var chunks = ReportBuilder.SplitMessage(text, maxLength: 500).ToList(); Assert.True(chunks.Count > 1); Assert.All(chunks, c => Assert.True(c.Length <= 500)); Assert.Equal(text, string.Join("\n", chunks)); } }