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);
}
}