using FluentAssertions; using Marathon.Domain.AnomalyDetection; using Marathon.Domain.Enums; namespace Marathon.Domain.Tests.AnomalyDetection; /// /// Unit tests for covering happy path, /// two-way (no draw), and malformed JSON tolerance. /// public sealed class AnomalyEvidenceParserTests { [Fact] public void Should_Parse_ThreeWayEvidence_With_DrawOutcome() { const string json = """ { "suspensionGapSeconds": 90, "preSuspension": { "capturedAt": "2026-05-10T18:00:00+03:00", "p1": 0.55, "pDraw": 0.20, "p2": 0.25, "rate1": 1.8, "rateDraw": 4.5, "rate2": 4.0 }, "postSuspension": { "capturedAt": "2026-05-10T18:02:30+03:00", "p1": 0.25, "pDraw": 0.20, "p2": 0.55, "rate1": 4.0, "rateDraw": 4.5, "rate2": 1.8 } } """; var parsed = AnomalyEvidenceParser.TryParse(json, out var data); parsed.Should().BeTrue(); data.SuspensionGapSeconds.Should().Be(90); data.PreSuspension.P1.Should().Be(0.55m); data.PreSuspension.PDraw.Should().Be(0.20m); data.PreSuspension.Favourite.Should().Be(Side.Side1, "Side1 had the highest pre-suspension probability"); data.PostSuspension.Favourite.Should().Be(Side.Side2, "Side2 became favourite after the flip"); } [Fact] public void Should_Parse_TwoWayEvidence_With_NullDraw() { const string json = """ { "suspensionGapSeconds": 75, "preSuspension": { "capturedAt": "2026-05-10T18:00:00+03:00", "p1": 0.70, "p2": 0.30, "rate1": 1.4, "rate2": 3.3 }, "postSuspension": { "capturedAt": "2026-05-10T18:01:30+03:00", "p1": 0.30, "p2": 0.70, "rate1": 3.3, "rate2": 1.4 } } """; var parsed = AnomalyEvidenceParser.TryParse(json, out var data); parsed.Should().BeTrue(); data.PreSuspension.PDraw.Should().BeNull("tennis has no draw outcome"); data.PreSuspension.RateDraw.Should().BeNull(); data.PreSuspension.Favourite.Should().Be(Side.Side1); data.PostSuspension.Favourite.Should().Be(Side.Side2); } [Fact] public void Should_ReturnFalse_When_JsonIsNullOrEmpty() { AnomalyEvidenceParser.TryParse(null, out _).Should().BeFalse(); AnomalyEvidenceParser.TryParse(string.Empty, out _).Should().BeFalse(); AnomalyEvidenceParser.TryParse(" ", out _).Should().BeFalse(); } [Fact] public void Should_ReturnFalse_When_JsonIsMalformed() { AnomalyEvidenceParser.TryParse("{not json", out _).Should().BeFalse(); } [Fact] public void Should_ReturnFalse_When_PreOrPostSuspensionMissing() { const string onlyPre = """ { "suspensionGapSeconds": 90, "preSuspension": { "capturedAt": "2026-05-10T18:00:00+03:00", "p1": 0.55, "p2": 0.25, "rate1": 1.8, "rate2": 4.0 } } """; AnomalyEvidenceParser.TryParse(onlyPre, out _).Should().BeFalse(); } [Fact] public void Favourite_Should_Be_Draw_When_DrawIsMostLikely() { const string json = """ { "suspensionGapSeconds": 60, "preSuspension": { "capturedAt": "2026-05-10T18:00:00+03:00", "p1": 0.30, "pDraw": 0.50, "p2": 0.20, "rate1": 3.3, "rateDraw": 2.0, "rate2": 5.0 }, "postSuspension": { "capturedAt": "2026-05-10T18:01:00+03:00", "p1": 0.30, "pDraw": 0.50, "p2": 0.20, "rate1": 3.3, "rateDraw": 2.0, "rate2": 5.0 } } """; AnomalyEvidenceParser.TryParse(json, out var data).Should().BeTrue(); data.PreSuspension.Favourite.Should().Be(Side.Draw); } }