using FluentAssertions; using Marathon.Domain.Enums; using Marathon.Domain.ValueObjects; using Marathon.Infrastructure.Scraping.Parsers; using Microsoft.Extensions.Logging.Abstractions; namespace Marathon.Infrastructure.Tests.Scraping; public sealed class EventOddsParserTests { private static string FixturePath(string filename) => Path.Combine( AppContext.BaseDirectory, "Fixtures", "marathonbet", filename); private readonly EventOddsParser _sut; public EventOddsParserTests() { var serverTime = new ServerTimeProvider(NullLogger.Instance); var periodMapper = new PeriodScopeMapper(basketballQuarterMode: false); _sut = new EventOddsParser( serverTime, periodMapper, NullLogger.Instance); } // ── Football fixture ─────────────────────────────────────────────────── [Fact] public async Task ParseAsync_FootballFixture_ReturnsNonNullSnapshot() { var html = await File.ReadAllTextAsync(FixturePath("event-football-sample.html")); var snapshot = await _sut.ParseAsync(html, OddsSource.PreMatch); snapshot.Should().NotBeNull(); } [Fact] public async Task ParseAsync_FootballFixture_SnapshotEventIdMatches() { var html = await File.ReadAllTextAsync(FixturePath("event-football-sample.html")); var snapshot = await _sut.ParseAsync(html, OddsSource.PreMatch); snapshot!.EventId.Value.Should().Be("26456117"); } [Fact] public async Task ParseAsync_FootballFixture_MatchWin1Extracted() { var html = await File.ReadAllTextAsync(FixturePath("event-football-sample.html")); var snapshot = await _sut.ParseAsync(html, OddsSource.PreMatch); var win1 = snapshot!.Bets.FirstOrDefault(b => b.Scope is MatchScope && b.Type == BetType.Win && b.Side == Side.Side1); win1.Should().NotBeNull("Match Win-1 bet must be present"); win1!.Rate.Value.Should().Be(1.65m); } [Fact] public async Task ParseAsync_FootballFixture_MatchDrawExtracted() { var html = await File.ReadAllTextAsync(FixturePath("event-football-sample.html")); var snapshot = await _sut.ParseAsync(html, OddsSource.PreMatch); var draw = snapshot!.Bets.FirstOrDefault(b => b.Scope is MatchScope && b.Type == BetType.Draw); draw.Should().NotBeNull("Match Draw bet must be present"); draw!.Rate.Value.Should().Be(4.1m); } [Fact] public async Task ParseAsync_FootballFixture_MatchWin2Extracted() { var html = await File.ReadAllTextAsync(FixturePath("event-football-sample.html")); var snapshot = await _sut.ParseAsync(html, OddsSource.PreMatch); var win2 = snapshot!.Bets.FirstOrDefault(b => b.Scope is MatchScope && b.Type == BetType.Win && b.Side == Side.Side2); win2.Should().NotBeNull("Match Win-2 bet must be present"); win2!.Rate.Value.Should().Be(5.7m); } [Fact] public async Task ParseAsync_FootballFixture_HandicapBetsExtracted() { var html = await File.ReadAllTextAsync(FixturePath("event-football-sample.html")); var snapshot = await _sut.ParseAsync(html, OddsSource.PreMatch); var fora1 = snapshot!.Bets.FirstOrDefault(b => b.Scope is MatchScope && b.Type == BetType.WinFora && b.Side == Side.Side1); var fora2 = snapshot.Bets.FirstOrDefault(b => b.Scope is MatchScope && b.Type == BetType.WinFora && b.Side == Side.Side2); fora1.Should().NotBeNull("Handicap Side1 must be present"); fora2.Should().NotBeNull("Handicap Side2 must be present"); fora1!.Value!.Value.Should().Be(-1.0m); fora1.Rate.Value.Should().Be(2.04m); fora2!.Value!.Value.Should().Be(1.0m); fora2.Rate.Value.Should().Be(1.82m); } [Fact] public async Task ParseAsync_FootballFixture_TotalBetsExtracted() { var html = await File.ReadAllTextAsync(FixturePath("event-football-sample.html")); var snapshot = await _sut.ParseAsync(html, OddsSource.PreMatch); var totalLess = snapshot!.Bets.FirstOrDefault(b => b.Scope is MatchScope && b.Type == BetType.Total && b.Side == Side.Less); var totalMore = snapshot.Bets.FirstOrDefault(b => b.Scope is MatchScope && b.Type == BetType.Total && b.Side == Side.More); totalLess.Should().NotBeNull("Total Less must be present"); totalMore.Should().NotBeNull("Total More must be present"); totalLess!.Value!.Value.Should().Be(2.5m); totalMore!.Value!.Value.Should().Be(2.5m); } [Fact] public async Task ParseAsync_FootballFixture_Period1WinExtracted() { var html = await File.ReadAllTextAsync(FixturePath("event-football-sample.html")); var snapshot = await _sut.ParseAsync(html, OddsSource.PreMatch); var p1Win1 = snapshot!.Bets.FirstOrDefault(b => b.Scope is PeriodScope { Number: 1 } && b.Type == BetType.Win && b.Side == Side.Side1); var p1Draw = snapshot.Bets.FirstOrDefault(b => b.Scope is PeriodScope { Number: 1 } && b.Type == BetType.Draw); var p1Win2 = snapshot.Bets.FirstOrDefault(b => b.Scope is PeriodScope { Number: 1 } && b.Type == BetType.Win && b.Side == Side.Side2); p1Win1.Should().NotBeNull("Period-1 Win-1 must be present for football"); p1Draw.Should().NotBeNull("Period-1 Draw must be present for football"); p1Win2.Should().NotBeNull("Period-1 Win-2 must be present for football"); } [Fact] public async Task ParseAsync_FootballFixture_SourceIsStamped() { var html = await File.ReadAllTextAsync(FixturePath("event-football-sample.html")); var snapshot = await _sut.ParseAsync(html, OddsSource.Live); snapshot!.Source.Should().Be(OddsSource.Live); } // ── Basketball fixture ───────────────────────────────────────────────── [Fact] public async Task ParseAsync_BasketballFixture_MatchWin1WithNoDrawExtracted() { var html = await File.ReadAllTextAsync(FixturePath("event-basketball-sample.html")); var snapshot = await _sut.ParseAsync(html, OddsSource.PreMatch); snapshot.Should().NotBeNull(); var win1 = snapshot!.Bets.FirstOrDefault(b => b.Scope is MatchScope && b.Type == BetType.Win && b.Side == Side.Side1); var draw = snapshot.Bets.FirstOrDefault(b => b.Scope is MatchScope && b.Type == BetType.Draw); win1.Should().NotBeNull("Basketball Match Win-1 must be present"); draw.Should().BeNull("Basketball (OT market) has no Draw outcome"); } [Fact] public async Task ParseAsync_BasketballFixture_Period1WinsExtracted() { var html = await File.ReadAllTextAsync(FixturePath("event-basketball-sample.html")); var snapshot = await _sut.ParseAsync(html, OddsSource.PreMatch); var p1Win1 = snapshot!.Bets.FirstOrDefault(b => b.Scope is PeriodScope { Number: 1 } && b.Type == BetType.Win && b.Side == Side.Side1); var p1Win2 = snapshot.Bets.FirstOrDefault(b => b.Scope is PeriodScope { Number: 1 } && b.Type == BetType.Win && b.Side == Side.Side2); p1Win1.Should().NotBeNull("Basketball Period-1 Win-1 must be present"); p1Win2.Should().NotBeNull("Basketball Period-1 Win-2 must be present"); } }