"""Unit tests for SentimentAnalyst.

This module tests the SentimentAnalyst class including sentiment analysis,
news collection, report generation, and cost deduction.
"""

import asyncio
from unittest.mock import patch

import pytest

from openclaw.agents.base import ActivityType
from openclaw.agents.sentiment_analyst import (
    SentimentAnalyst,
    SentimentReport,
    SentimentSource,
)
from openclaw.core.economy import SurvivalStatus


class TestSentimentAnalystInitialization:
    """Test SentimentAnalyst initialization."""

    def test_default_initialization(self):
        """Test agent with default parameters."""
        agent = SentimentAnalyst(agent_id="sentiment-1", initial_capital=10000.0)

        assert agent.agent_id == "sentiment-1"
        assert agent.balance == 10000.0
        assert agent.skill_level == 0.5
        assert agent.max_sources == 10
        assert agent._analysis_history == []
        assert agent.decision_cost == 0.08

    def test_custom_initialization(self):
        """Test agent with custom parameters."""
        agent = SentimentAnalyst(
            agent_id="sentiment-2",
            initial_capital=5000.0,
            skill_level=0.8,
            max_sources=15,
        )

        assert agent.agent_id == "sentiment-2"
        assert agent.balance == 5000.0
        assert agent.skill_level == 0.8
        assert agent.max_sources == 15

    def test_inherits_from_base_agent(self):
        """Test that SentimentAnalyst inherits from BaseAgent."""
        from openclaw.agents.base import BaseAgent

        agent = SentimentAnalyst(agent_id="test", initial_capital=10000.0)

        assert isinstance(agent, BaseAgent)


class TestDecideActivity:
    """Test decide_activity method."""

    @pytest.fixture
    def agent(self):
        """Create a test agent."""
        return SentimentAnalyst(agent_id="test", initial_capital=10000.0)

    def test_bankrupt_agent_only_rests(self, agent):
        """Test that bankrupt agent can only rest."""
        agent.economic_tracker.balance = 0  # Bankrupt

        result = asyncio.run(agent.decide_activity())

        assert result == ActivityType.REST

    def test_critical_status_prefers_learning(self, agent):
        """Test critical status leads to learning."""
        agent.economic_tracker.balance = 3500.0  # Critical
        agent.state.skill_level = 0.5

        result = asyncio.run(agent.decide_activity())

        assert result in [ActivityType.LEARN, ActivityType.PAPER_TRADE]

    def test_thriving_status_prefers_analyzing(self, agent):
        """Test thriving status leads to analyzing."""
        agent.economic_tracker.balance = 20000.0  # Thriving

        # Run multiple times to account for randomness
        results = [asyncio.run(agent.decide_activity()) for _ in range(20)]

        # Most should be ANALYZE
        analyze_count = results.count(ActivityType.ANALYZE)
        assert analyze_count >= 10  # At least half

    def test_struggling_status_less_analyzing(self, agent):
        """Test struggling status prefers paper trading."""
        agent.economic_tracker.balance = 8500.0  # Struggling

        # Run multiple times
        results = [asyncio.run(agent.decide_activity()) for _ in range(20)]

        # Some should be paper trade
        paper_trades = [r for r in results if r == ActivityType.PAPER_TRADE]
        assert len(paper_trades) >= 3


class TestAnalyzeSentiment:
    """Test analyze_sentiment method."""

    @pytest.fixture
    def agent(self):
        """Create a test agent."""
        return SentimentAnalyst(agent_id="test", initial_capital=10000.0)

    def test_returns_sentiment_report(self, agent):
        """Test that analyze_sentiment returns SentimentReport."""
        result = asyncio.run(agent.analyze_sentiment("AAPL"))

        assert isinstance(result, SentimentReport)
        assert result.symbol == "AAPL"
        assert result.overall_sentiment in ["bullish", "bearish", "neutral"]
        assert -1.0 <= result.sentiment_score <= 1.0
        assert len(result.sources) > 0
        assert result.summary != ""

    def test_deducts_decision_cost(self, agent):
        """Test that analyze_sentiment deducts $0.08 decision cost."""
        initial_balance = agent.balance

        asyncio.run(agent.analyze_sentiment("AAPL"))

        # Should deduct $0.08 + additional token/data costs
        assert agent.balance < initial_balance
        # At minimum, the $0.08 should be deducted
        assert agent.balance <= initial_balance - 0.08

    def test_exact_decision_cost_deducted(self, agent):
        """Test that exactly $0.08 decision cost is deducted."""
        initial_balance = agent.balance

        # Calculate expected cost
        expected_cost = agent.decision_cost  # $0.08

        asyncio.run(agent.analyze_sentiment("AAPL"))

        # The balance change should be at least the decision cost
        balance_change = initial_balance - agent.balance
        # Use approximate comparison due to floating point precision
        assert balance_change >= expected_cost - 0.001  # Allow small floating point tolerance

    def test_sentiment_score_in_range(self, agent):
        """Test that sentiment score is between -1.0 and 1.0."""
        result = asyncio.run(agent.analyze_sentiment("TSLA"))

        assert -1.0 <= result.sentiment_score <= 1.0

    def test_confidence_in_range(self, agent):
        """Test that confidence is between 0.0 and 1.0."""
        result = asyncio.run(agent.analyze_sentiment("NVDA"))

        assert 0.0 < result.confidence <= 1.0

    def test_sources_populated(self, agent):
        """Test that sources are populated in the report."""
        result = asyncio.run(agent.analyze_sentiment("MSFT"))

        assert len(result.sources) > 0
        assert all(isinstance(s, SentimentSource) for s in result.sources)
        assert all(s.title for s in result.sources)
        assert all(s.source for s in result.sources)

    def test_sample_headlines_populated(self, agent):
        """Test that sample headlines are populated."""
        result = asyncio.run(agent.analyze_sentiment("GOOGL"))

        assert len(result.sample_headlines) > 0
        assert len(result.sample_headlines) <= 3

    def test_timestamp_populated(self, agent):
        """Test that timestamp is populated."""
        result = asyncio.run(agent.analyze_sentiment("AMZN"))

        assert result.timestamp != ""
        assert "T" in result.timestamp  # ISO format has T

    def test_history_recorded(self, agent):
        """Test that analysis is recorded in history."""
        initial_history_len = len(agent._analysis_history)

        asyncio.run(agent.analyze_sentiment("META"))

        assert len(agent._analysis_history) == initial_history_len + 1
        assert agent._analysis_history[-1].symbol == "META"


class TestAnalyze:
    """Test analyze method (async, from BaseAgent)."""

    @pytest.fixture
    def agent(self):
        """Create a test agent."""
        return SentimentAnalyst(agent_id="test", initial_capital=10000.0)

    def test_analyze_returns_dict(self, agent):
        """Test that analyze returns a dictionary."""
        result = asyncio.run(agent.analyze("AAPL"))

        assert isinstance(result, dict)
        assert result["symbol"] == "AAPL"
        assert "sentiment" in result
        assert "score" in result
        assert "confidence" in result
        assert "summary" in result
        assert "sources_analyzed" in result
        assert "cost" in result

    def test_analyze_deducts_cost(self, agent):
        """Test that analyze deducts cost."""
        initial_balance = agent.balance

        asyncio.run(agent.analyze("AAPL"))

        assert agent.balance < initial_balance


class TestCollectNewsData:
    """Test _collect_news_data method."""

    @pytest.fixture
    def agent(self):
        """Create a test agent."""
        return SentimentAnalyst(agent_id="test", initial_capital=10000.0)

    def test_returns_list_of_sources(self, agent):
        """Test that method returns list of SentimentSource."""
        sources = agent._collect_news_data("AAPL")

        assert isinstance(sources, list)
        assert len(sources) > 0
        assert all(isinstance(s, SentimentSource) for s in sources)

    def test_sources_within_max_limit(self, agent):
        """Test that sources don't exceed max_sources."""
        sources = agent._collect_news_data("TSLA")

        assert len(sources) <= agent.max_sources

    def test_sources_have_required_fields(self, agent):
        """Test that sources have required fields."""
        sources = agent._collect_news_data("NVDA")

        for source in sources:
            assert source.title != ""
            assert source.source in ["Reuters", "Bloomberg", "CNBC", "WSJ", "TechCrunch"]
            assert 0.0 <= source.relevance_score <= 1.0
            assert source.raw_sentiment in ["positive", "negative", "neutral"]


class TestCalculateSentiment:
    """Test _calculate_sentiment method."""

    @pytest.fixture
    def agent(self):
        """Create a test agent."""
        return SentimentAnalyst(agent_id="test", initial_capital=10000.0)

    def test_bullish_sources_positive_score(self, agent):
        """Test that bullish sources produce positive score."""
        sources = [
            SentimentSource(
                title="Stock surges on strong earnings growth",
                content="Company reports record profit and expansion",
                source="Reuters",
                timestamp="2024-01-01T00:00:00",
                raw_sentiment="positive",
                relevance_score=1.0,
            )
        ]

        score, confidence = agent._calculate_sentiment(sources)

        assert score > 0
        assert 0.0 < confidence <= 1.0

    def test_bearish_sources_negative_score(self, agent):
        """Test that bearish sources produce negative score."""
        sources = [
            SentimentSource(
                title="Stock crashes amid bankruptcy fears",
                content="Company faces major losses and layoffs",
                source="Reuters",
                timestamp="2024-01-01T00:00:00",
                raw_sentiment="negative",
                relevance_score=1.0,
            )
        ]

        score, confidence = agent._calculate_sentiment(sources)

        assert score < 0
        assert 0.0 < confidence <= 1.0

    def test_neutral_sources_near_zero_score(self, agent):
        """Test that neutral sources produce near-zero score."""
        sources = [
            SentimentSource(
                title="Company announces regular meeting",
                content="Standard board meeting scheduled",
                source="Reuters",
                timestamp="2024-01-01T00:00:00",
                raw_sentiment="neutral",
                relevance_score=0.5,
            )
        ]

        score, confidence = agent._calculate_sentiment(sources)

        assert -0.5 <= score <= 0.5

    def test_empty_sources_returns_zero(self, agent):
        """Test that empty sources return zero score."""
        score, confidence = agent._calculate_sentiment([])

        assert score == 0.0
        assert confidence == 0.0


class TestGenerateSummary:
    """Test _generate_summary method."""

    @pytest.fixture
    def agent(self):
        """Create a test agent."""
        return SentimentAnalyst(agent_id="test", initial_capital=10000.0)

    def test_bullish_summary(self, agent):
        """Test summary generation for bullish sentiment."""
        sources = [
            SentimentSource(title="Good news", content="", source="A", timestamp="", raw_sentiment="positive"),
            SentimentSource(title="Good news", content="", source="B", timestamp="", raw_sentiment="positive"),
        ]

        summary = agent._generate_summary("AAPL", "bullish", 0.5, sources)

        assert "bullish" in summary.lower()
        assert "AAPL" in summary
        assert "0.50" in summary or "0.5" in summary

    def test_bearish_summary(self, agent):
        """Test summary generation for bearish sentiment."""
        sources = [
            SentimentSource(title="Bad news", content="", source="A", timestamp="", raw_sentiment="negative"),
        ]

        summary = agent._generate_summary("TSLA", "bearish", -0.3, sources)

        assert "bearish" in summary.lower()
        assert "TSLA" in summary

    def test_neutral_summary(self, agent):
        """Test summary generation for neutral sentiment."""
        sources = [
            SentimentSource(title="News", content="", source="A", timestamp="", raw_sentiment="neutral"),
        ]

        summary = agent._generate_summary("NVDA", "neutral", 0.0, sources)

        assert "neutral" in summary.lower()
        assert "NVDA" in summary


class TestGetAnalysisHistory:
    """Test get_analysis_history method."""

    @pytest.fixture
    def agent(self):
        """Create a test agent."""
        return SentimentAnalyst(agent_id="test", initial_capital=10000.0)

    def test_returns_copy(self, agent):
        """Test that method returns a copy of history."""
        asyncio.run(agent.analyze_sentiment("AAPL"))

        history = agent.get_analysis_history()
        history.append(None)  # Modify the copy

        # Original should be unchanged
        assert len(agent._analysis_history) == 1

    def test_returns_all_analyses(self, agent):
        """Test that method returns all analyses."""
        asyncio.run(agent.analyze_sentiment("AAPL"))
        asyncio.run(agent.analyze_sentiment("TSLA"))

        history = agent.get_analysis_history()

        assert len(history) == 2


class TestGetSentimentTrend:
    """Test get_sentiment_trend method."""

    @pytest.fixture
    def agent(self):
        """Create a test agent."""
        return SentimentAnalyst(agent_id="test", initial_capital=10000.0)

    def test_returns_none_for_no_history(self, agent):
        """Test that method returns None when no history."""
        result = agent.get_sentiment_trend("AAPL")

        assert result is None

    def test_returns_trend_data(self, agent):
        """Test that method returns trend data."""
        # Create multiple analyses for same symbol
        with patch.object(agent, '_collect_news_data') as mock_collect:
            mock_collect.return_value = [
                SentimentSource(
                    title="Positive news",
                    content="",
                    source="Reuters",
                    timestamp="",
                    raw_sentiment="positive",
                    relevance_score=1.0,
                )
            ]
            asyncio.run(agent.analyze_sentiment("AAPL"))
            asyncio.run(agent.analyze_sentiment("AAPL"))

        result = agent.get_sentiment_trend("AAPL")

        assert result is not None
        assert result["symbol"] == "AAPL"
        assert "average_score" in result
        assert "trend" in result
        assert "analyses_count" in result
        assert "latest_sentiment" in result

    def test_trend_improving(self, agent):
        """Test that trend shows improving when scores increase."""
        # Manually add analyses with increasing scores
        agent._analysis_history.append(
            SentimentReport(
                symbol="AAPL",
                overall_sentiment="neutral",
                sentiment_score=0.0,
                sources=[],
                summary="",
            )
        )
        agent._analysis_history.append(
            SentimentReport(
                symbol="AAPL",
                overall_sentiment="bullish",
                sentiment_score=0.5,
                sources=[],
                summary="",
            )
        )
        agent._analysis_history.append(
            SentimentReport(
                symbol="AAPL",
                overall_sentiment="bullish",
                sentiment_score=0.8,
                sources=[],
                summary="",
            )
        )

        result = agent.get_sentiment_trend("AAPL")

        assert result["trend"] == "improving"


class TestSentimentSource:
    """Test SentimentSource dataclass."""

    def test_creation(self):
        """Test creating a SentimentSource."""
        source = SentimentSource(
            title="Test Title",
            content="Test Content",
            source="Reuters",
            timestamp="2024-01-01T00:00:00",
            raw_sentiment="positive",
            relevance_score=0.8,
        )

        assert source.title == "Test Title"
        assert source.content == "Test Content"
        assert source.source == "Reuters"
        assert source.raw_sentiment == "positive"
        assert source.relevance_score == 0.8

    def test_default_values(self):
        """Test default values for SentimentSource."""
        source = SentimentSource(
            title="Test",
            content="",
            source="Reuters",
            timestamp="",
        )

        assert source.raw_sentiment == ""
        assert source.relevance_score == 0.5


class TestSentimentReport:
    """Test SentimentReport dataclass."""

    def test_creation(self):
        """Test creating a SentimentReport."""
        report = SentimentReport(
            symbol="AAPL",
            overall_sentiment="bullish",
            sentiment_score=0.75,
            sources=[],
            summary="Positive outlook",
            confidence=0.8,
            sample_headlines=["Good news"],
        )

        assert report.symbol == "AAPL"
        assert report.overall_sentiment == "bullish"
        assert report.sentiment_score == 0.75
        assert report.summary == "Positive outlook"
        assert report.confidence == 0.8

    def test_default_values(self):
        """Test default values for SentimentReport."""
        report = SentimentReport(
            symbol="TSLA",
            overall_sentiment="neutral",
            sentiment_score=0.0,
            sources=[],
            summary="",
        )

        assert report.timestamp != ""
        assert report.confidence == 0.5
        assert report.sample_headlines == []


class TestDecisionCost:
    """Test decision_cost class attribute."""

    def test_decision_cost_value(self):
        """Test that decision_cost is $0.08."""
        agent = SentimentAnalyst(agent_id="test", initial_capital=10000.0)

        assert agent.decision_cost == 0.08

    def test_decision_cost_class_attribute(self):
        """Test that decision_cost is a class attribute."""
        assert SentimentAnalyst.decision_cost == 0.08
