package linguistics import ( "context" "errors" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // Mocks for provider interfaces type mockLangDetector struct { lang string err error } func (m mockLangDetector) DetectLanguage(text string) (string, error) { return m.lang, m.err } type mockSentimentProvider struct { score float64 err error } func (m mockSentimentProvider) Score(text string, language string) (float64, error) { return m.score, m.err } type mockKeywordProvider struct { kws []Keyword err error } func (m mockKeywordProvider) Extract(text string, language string) ([]Keyword, error) { return m.kws, m.err } func TestAnalyzeText_Empty(t *testing.T) { a := NewBasicTextAnalyzer() res, err := a.AnalyzeText(context.Background(), "", "") require.NoError(t, err) assert.NotNil(t, res) assert.Equal(t, 0, res.WordCount) assert.Equal(t, 0, res.SentenceCount) assert.Equal(t, 0.0, res.Sentiment) assert.Len(t, res.Keywords, 0) } func TestAnalyzeText_ProvidersAndLangDetection(t *testing.T) { // Arrange a := NewBasicTextAnalyzer(). WithLanguageDetector(mockLangDetector{lang: "en", err: nil}). WithSentimentProvider(mockSentimentProvider{score: 0.75}). WithKeywordProvider(mockKeywordProvider{kws: []Keyword{{Text: "golang", Relevance: 0.42}}}) text := "Go is great. Go makes concurrency easier." // Act res, err := a.AnalyzeText(context.Background(), text, "") // Assert require.NoError(t, err) require.NotNil(t, res) assert.InDelta(t, 0.75, res.Sentiment, 1e-9) require.Len(t, res.Keywords, 1) assert.Equal(t, "golang", res.Keywords[0].Text) assert.InDelta(t, 0.42, res.Keywords[0].Relevance, 1e-9) // Basic stats make sense assert.Greater(t, res.WordCount, 0) assert.Greater(t, res.SentenceCount, 0) // Readability is clamped to [0,100] assert.GreaterOrEqual(t, res.ReadabilityScore, 0.0) assert.LessOrEqual(t, res.ReadabilityScore, 100.0) assert.Equal(t, "Simplified Flesch-Kincaid", res.ReadabilityMethod) } func TestAnalyzeText_FallbackOnProviderError(t *testing.T) { // Arrange providers that fail so analyzer uses internal fallbacks a := NewBasicTextAnalyzer(). WithSentimentProvider(mockSentimentProvider{err: errors.New("boom")}). WithKeywordProvider(mockKeywordProvider{err: errors.New("boom")}) text := "I love good code but hate terrible bugs." // Act res, err := a.AnalyzeText(context.Background(), text, "en") // Assert require.NoError(t, err) require.NotNil(t, res) // Fallback sentiment should be between -1 and 1; with mixed words it should be non-zero assert.GreaterOrEqual(t, res.Sentiment, -1.0) assert.LessOrEqual(t, res.Sentiment, 1.0) // Keywords should come from fallback extractor and be non-empty for this text assert.NotEmpty(t, res.Keywords) } func TestAnalyzeTextConcurrently_AggregatesWithProviders(t *testing.T) { // Providers return consistent values regardless of input kw := []Keyword{{Text: "constant", Relevance: 0.3}} a := NewBasicTextAnalyzer(). WithLanguageDetector(mockLangDetector{lang: "en", err: nil}). WithSentimentProvider(mockSentimentProvider{score: 0.5}). WithKeywordProvider(mockKeywordProvider{kws: kw}) text := "One sentence. Another sentence! And a question? Final one." // Act _, err1 := a.AnalyzeText(context.Background(), text, "") conc, err2 := a.AnalyzeTextConcurrently(context.Background(), text, "", 3) // Assert require.NoError(t, err1) require.NoError(t, err2) // Basic stats: should be sane assert.Greater(t, conc.WordCount, 0) assert.GreaterOrEqual(t, conc.SentenceCount, 0) assert.GreaterOrEqual(t, conc.ParagraphCount, 1) assert.GreaterOrEqual(t, conc.AvgWordLength, 0.0) assert.GreaterOrEqual(t, conc.AvgSentenceLength, 0.0) // Readability is clamped to [0,100] assert.GreaterOrEqual(t, conc.ReadabilityScore, 0.0) assert.LessOrEqual(t, conc.ReadabilityScore, 100.0) assert.Equal(t, "Simplified Flesch-Kincaid", conc.ReadabilityMethod) // Provider-driven outputs should align assert.InDelta(t, 0.5, conc.Sentiment, 1e-9) require.Len(t, conc.Keywords, 1) assert.Equal(t, "constant", conc.Keywords[0].Text) assert.InDelta(t, 0.3, conc.Keywords[0].Relevance, 1e-9) } func TestAnalyzeTextConcurrently_ContextCanceled(t *testing.T) { a := NewBasicTextAnalyzer(). WithLanguageDetector(mockLangDetector{lang: "en", err: nil}). WithSentimentProvider(mockSentimentProvider{score: 0.9}). WithKeywordProvider(mockKeywordProvider{kws: []Keyword{{Text: "x", Relevance: 0.1}}}) text := "This should not be processed. Another sentence. And one more." ctx, cancel := context.WithCancel(context.Background()) cancel() // cancel immediately before processing conc, err := a.AnalyzeTextConcurrently(ctx, text, "", 4) require.NoError(t, err) require.NotNil(t, conc) // With immediate cancellation, goroutines should early-return and no values should be sent assert.Equal(t, 0, conc.WordCount) assert.Equal(t, 0, conc.SentenceCount) assert.Equal(t, 0, conc.ParagraphCount) assert.Equal(t, 0.0, conc.AvgWordLength) assert.Equal(t, 0.0, conc.AvgSentenceLength) // Readability is clamped [0,100]; with zero stats it becomes 100 assert.GreaterOrEqual(t, conc.ReadabilityScore, 0.0) assert.LessOrEqual(t, conc.ReadabilityScore, 100.0) assert.Empty(t, conc.Keywords) assert.Equal(t, 0.0, conc.Sentiment) }