using FluentAssertions; using SIGCM2.Domain.Pricing.Exceptions; using SIGCM2.Domain.Pricing.WordCounter; namespace SIGCM2.Application.Tests.Domain.Pricing.WordCounter; /// /// PRC-001 — C5 Golden Cases Suite. /// 25 canonical test cases that constitute the SPIKE acceptance gate. /// Each golden case is a separate [Theory] row or [Fact] — individually identified. /// public sealed class WordCounterGoldenCasesTests { private static readonly WordCounterService _svc = new(); // ───────────────────────────────────────────────────────────────────────── // GC-01: plain text — 4 words, no specials // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC01_PlainText_FourWords_NoSpecials() { var result = _svc.Count("vendo auto ford 2005"); result.TotalWords.Should().Be(4); result.SpecialCharCounts.Should().BeEmpty(); } // ───────────────────────────────────────────────────────────────────────── // GC-02: dollar splits a token — 3 words, Currency=1 // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC02_DollarSign_SplitsToken_ThreeWords_CurrencyOne() { var result = _svc.Count("vendo auto $5000"); result.TotalWords.Should().Be(3); result.SpecialCharCounts.GetValueOrDefault("Currency").Should().Be(1); } // ───────────────────────────────────────────────────────────────────────── // GC-03: percentage — 2 words, Percentage=1 // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC03_Percentage_TwoWords_PercentageOne() { var result = _svc.Count("descuento 20%"); result.TotalWords.Should().Be(2); result.SpecialCharCounts.GetValueOrDefault("Percentage").Should().Be(1); } // ───────────────────────────────────────────────────────────────────────── // GC-04: exclamation mark — 5 words, Exclamation=1 // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC04_Exclamation_FiveWords_ExclamationOne() { var result = _svc.Count("OFERTA! no te la pierdas"); result.TotalWords.Should().Be(5); result.SpecialCharCounts.GetValueOrDefault("Exclamation").Should().Be(1); } // ───────────────────────────────────────────────────────────────────────── // GC-05: inverse + normal exclamation — 1 word, Exclamation=2 // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC05_InverseAndNormalExclamation_OneWord_ExclamationTwo() { var result = _svc.Count("¡Oferta!"); result.TotalWords.Should().Be(1); result.SpecialCharCounts.GetValueOrDefault("Exclamation").Should().Be(2); } // ───────────────────────────────────────────────────────────────────────── // GC-06: anti-fraud pattern P$a$l$a$b$r$a → 7 words, Currency=6 // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC06_AntiFraudDollarPattern_SevenWords_CurrencySix() { var result = _svc.Count("P$a$l$a$b$r$a"); result.TotalWords.Should().Be(7); result.SpecialCharCounts.GetValueOrDefault("Currency").Should().Be(6); } // ───────────────────────────────────────────────────────────────────────── // GC-07: mixed specials + hyphen split // VENDO | auto | 5000 | usado | 90 | buen | estado = 7 // Exclamation=1, Currency=1, Percentage=1 // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC07_MixedSpecialsAndHyphen_SevenWords_OneEachCategory() { var result = _svc.Count("VENDO! auto $5000 usado %90 buen-estado"); result.TotalWords.Should().Be(7); result.SpecialCharCounts.GetValueOrDefault("Exclamation").Should().Be(1); result.SpecialCharCounts.GetValueOrDefault("Currency").Should().Be(1); result.SpecialCharCounts.GetValueOrDefault("Percentage").Should().Be(1); } // ───────────────────────────────────────────────────────────────────────── // GC-08: multi-space collapses — 3 words, no specials // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC08_MultiSpace_Collapses_ThreeWords_NoSpecials() { var result = _svc.Count("vendo auto ford"); result.TotalWords.Should().Be(3); result.SpecialCharCounts.Should().BeEmpty(); } // ───────────────────────────────────────────────────────────────────────── // GC-09: CRLF becomes space — 4 words // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC09_CRLF_BecomesSpace_FourWords() { var result = _svc.Count("vendo auto\r\nbuen estado"); result.TotalWords.Should().Be(4); result.SpecialCharCounts.Should().BeEmpty(); } // ───────────────────────────────────────────────────────────────────────── // GC-10: leading/trailing whitespace stripped — 2 words // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC10_LeadingTrailingWhitespace_Stripped_TwoWords() { var result = _svc.Count(" vendo auto "); result.TotalWords.Should().Be(2); result.SpecialCharCounts.Should().BeEmpty(); } // ───────────────────────────────────────────────────────────────────────── // GC-11: tildes are regular letters — 4 words, no specials // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC11_Tildes_AreRegularLetters_FourWords_NoSpecials() { var result = _svc.Count("vendo máquina niño año"); result.TotalWords.Should().Be(4); result.SpecialCharCounts.Should().BeEmpty(); } // ───────────────────────────────────────────────────────────────────────── // GC-12: ñ in words — 3 words, no specials // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC12_EnneInWords_ThreeWords_NoSpecials() { var result = _svc.Count("año mañana niño"); result.TotalWords.Should().Be(3); result.SpecialCharCounts.Should().BeEmpty(); } // ───────────────────────────────────────────────────────────────────────── // GC-13: empty string — 0 words, empty dict // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC13_EmptyString_ZeroWords_EmptyDict() { var result = _svc.Count(""); result.TotalWords.Should().Be(0); result.SpecialCharCounts.Should().BeEmpty(); } // ───────────────────────────────────────────────────────────────────────── // GC-14: whitespace-only — 0 words, empty dict // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC14_WhitespaceOnly_ZeroWords_EmptyDict() { var result = _svc.Count(" "); result.TotalWords.Should().Be(0); result.SpecialCharCounts.Should().BeEmpty(); } // ───────────────────────────────────────────────────────────────────────── // GC-15: single word — 1 word // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC15_SingleWord_OneWord() { var result = _svc.Count("auto"); result.TotalWords.Should().Be(1); result.SpecialCharCounts.Should().BeEmpty(); } // ───────────────────────────────────────────────────────────────────────── // GC-16: numbers and tilde — 3 words // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC16_NumbersAndTilde_ThreeWords() { var result = _svc.Count("1978 2005 año"); result.TotalWords.Should().Be(3); result.SpecialCharCounts.Should().BeEmpty(); } // ───────────────────────────────────────────────────────────────────────── // GC-17: URL treated as single token — 2 words // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC17_Url_TreatedAsSingleToken_TwoWords() { var result = _svc.Count("visita www.example.com"); result.TotalWords.Should().Be(2); result.SpecialCharCounts.Should().BeEmpty(); } // ───────────────────────────────────────────────────────────────────────── // GC-18: hyphenated compounds split — 4 words // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC18_HyphenatedCompounds_SplitIntoFourWords() { var result = _svc.Count("buen-estado casi-nuevo"); result.TotalWords.Should().Be(4); result.SpecialCharCounts.Should().BeEmpty(); } // ───────────────────────────────────────────────────────────────────────── // GC-19: emoji at end — throws EmojiDetectedException // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC19_EmojiAtEnd_ThrowsEmojiDetectedException() { var act = () => _svc.Count("vendo 🚗"); act.Should().Throw(); } // ───────────────────────────────────────────────────────────────────────── // GC-20: emoji in middle — throws EmojiDetectedException // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC20_EmojiInMiddle_ThrowsEmojiDetectedException() { var act = () => _svc.Count("vendo auto 🚗 2005"); act.Should().Throw(); } // ───────────────────────────────────────────────────────────────────────── // GC-21: only emoji — throws EmojiDetectedException // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC21_OnlyEmoji_ThrowsEmojiDetectedException() { var act = () => _svc.Count("🚗"); act.Should().Throw(); } // ───────────────────────────────────────────────────────────────────────── // GC-22: exactly 2000 chars — passes (no exception), TotalWords >= 1 // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC22_Exactly2000Chars_PassesValidation() { // Build a 2000-char string of "a " repeated (1000 times = 2000 chars) var input = string.Concat(Enumerable.Repeat("a ", 1000)).TrimEnd(); // 1999 chars — add one more // Ensure exactly 2000: "a " x 999 = 1998 + "aa" = 2000 input = string.Concat(Enumerable.Repeat("a ", 999)) + "aa"; // 999*2 + 2 = 2000 var act = () => _svc.Count(input); act.Should().NotThrow(); var result = _svc.Count(input); result.TotalWords.Should().BeGreaterThanOrEqualTo(1); } // ───────────────────────────────────────────────────────────────────────── // GC-23: 2001 chars — throws WordCountValidationException // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC23_TwoThousandAndOneChars_ThrowsWordCountValidationException() { var input = new string('a', 2001); var act = () => _svc.Count(input); act.Should().Throw(); } // ───────────────────────────────────────────────────────────────────────── // GC-24: all specials replaced → 0 words // "$$ %% !! ¡¡" → Currency=2, Percentage=2, Exclamation=4, TotalWords=0 // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC24_AllSpecials_ZeroWordsRemain_CorrectCounts() { var result = _svc.Count("$$ %% !! ¡¡"); result.TotalWords.Should().Be(0); result.SpecialCharCounts.GetValueOrDefault("Currency").Should().Be(2); result.SpecialCharCounts.GetValueOrDefault("Percentage").Should().Be(2); result.SpecialCharCounts.GetValueOrDefault("Exclamation").Should().Be(4); } // ───────────────────────────────────────────────────────────────────────── // GC-25: anti-fraud embedded in sentence — 9 words, Currency=6 // ───────────────────────────────────────────────────────────────────────── [Fact] public void GC25_AntiFraudEmbedded_NineWords_CurrencySix() { // "vendo P$a$l$a$b$r$a usado" // P$a$l$a$b$r$a → P a l a b r a = 7 tokens; + vendo + usado = 9 var result = _svc.Count("vendo P$a$l$a$b$r$a usado"); result.TotalWords.Should().Be(9); result.SpecialCharCounts.GetValueOrDefault("Currency").Should().Be(6); } }