using SQLInsight.Contracts; using SQLInsight.Hub.Support; namespace SQLInsight.IntegrationTests; /// Pure logic behind support cases: no database, so these run with the unit tests. public class HubSupportLogicTests { [Fact] public void First_case_number_is_SUP_000001() => Assert.Equal("SUP-000001", CaseNumbers.Next([])); [Fact] public void Next_number_follows_the_highest_SUP_number() => Assert.Equal("SUP-000013", CaseNumbers.Next(["SUP-000002", "sup-000012", "SUP-000009"])); [Fact] public void Hand_typed_references_do_not_affect_the_sequence() => Assert.Equal("SUP-000004", CaseNumbers.Next(["SUP-000003", "INC-99999", "SUP-12A", "Sag fra telefon"])); [Fact] public void Old_short_numbers_continue_in_six_digits() => Assert.Equal("SUP-001235", CaseNumbers.Next(["SUP-1234"])); private static readonly DateTime T0 = new(2026, 9, 1, 0, 0, 0, DateTimeKind.Utc); private static SupportReportCase Case(int id, int customer, double createdDays, double? closedAfterHours, string source = SupportCaseSources.Staff) => new(id, customer, $"Kunde {customer}", $"SUP-{id:D6}", "Titel", source, T0.AddDays(createdDays), closedAfterHours is { } h ? T0.AddDays(createdDays).AddHours(h) : null); [Fact] public void Report_counts_created_closed_and_open_and_computes_average_and_median() { var cases = new[] { Case(1, 1, 1, 2, SupportCaseSources.Customer), Case(2, 1, 2, 4), Case(3, 1, 3, 30), Case(4, 1, 4, null, SupportCaseSources.Customer), Case(5, 2, -40, 1), // created and closed before the period: not counted Case(6, 2, -40, null), // created before, still open: open now }; var report = SupportReport.Build(cases, T0, T0.AddDays(30), T0.AddDays(10)); var first = report.Customers.Single(c => c.CustomerId == 1); var second = report.Customers.Single(c => c.CustomerId == 2); Assert.Equal((4, 3, 1), (first.Created, first.Closed, first.OpenNow)); Assert.Equal(TimeSpan.FromHours(12), first.AverageResolution); Assert.Equal(TimeSpan.FromHours(4), first.MedianResolution); Assert.Equal(TimeSpan.FromDays(6), first.OldestOpenAge); Assert.Equal(0.5, first.CustomerCreatedShare); Assert.Equal((0, 0, 1), (second.Created, second.Closed, second.OpenNow)); Assert.Equal(TimeSpan.FromDays(50), second.OldestOpenAge); Assert.Equal((4, 3, 2), (report.Total.Created, report.Total.Closed, report.Total.OpenNow)); Assert.Equal([4, 3, 2, 1], report.Cases.Select(c => c.Id)); } [Fact] public void Median_of_an_even_count_is_the_mean_of_the_middle_two() => Assert.Equal(TimeSpan.FromHours(3), SupportReport.Median([TimeSpan.FromHours(1), TimeSpan.FromHours(2), TimeSpan.FromHours(4), TimeSpan.FromHours(9)])); [Theory] [InlineData(30, "30 min")] [InlineData(90, "1,5 t")] [InlineData(4320, "3 dage")] public void Durations_are_readable(double minutes, string expected) => Assert.Equal(expected, SupportReport.Duration(TimeSpan.FromMinutes(minutes))); [Fact] public void Default_period_is_the_last_90_days_including_today() { var (from, to) = SupportReportPeriod.Resolve(null, null, new DateTime(2026, 10, 1, 14, 0, 0, DateTimeKind.Utc)); Assert.Equal(new DateTime(2026, 10, 2, 0, 0, 0, DateTimeKind.Utc), to); Assert.Equal(to.AddDays(-90), from); } [Fact] public void Csv_has_bom_semicolons_and_defuses_formulas() { var report = SupportReport.Build([Case(1, 1, 1, 6) with { Title = "=HYPERLINK(\"x\"); ond" }], T0, T0.AddDays(30), T0.AddDays(10)); var bytes = SupportReport.Csv(report); var text = System.Text.Encoding.UTF8.GetString(bytes, 3, bytes.Length - 3); Assert.Equal([0xEF, 0xBB, 0xBF], bytes[..3]); Assert.StartsWith("Sag;Kunde;Titel;Kilde;", text, StringComparison.Ordinal); Assert.Contains("\"'=HYPERLINK(\"\"x\"\"); ond\"", text, StringComparison.Ordinal); Assert.Contains(";6,0\r\n", text, StringComparison.Ordinal); } [Fact] public void Request_validation_rejects_missing_contact_and_long_titles() { var valid = new SupportCaseRequest(Guid.NewGuid(), "Langsom server", null, "Anna", "anna@kunde.dk", null, null); Assert.Null(valid.Validate()); Assert.NotNull((valid with { ContactEmail = "anna" }).Validate()); Assert.NotNull((valid with { Title = new string('x', 201) }).Validate()); Assert.NotNull((valid with { ClientRequestId = Guid.Empty }).Validate()); } }