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