"""Unit tests for BullResearcher Agent.

This module tests the BullResearcher class including bull case generation,
counter-arguments, price targets, and decision cost deduction.
"""

import asyncio
from unittest.mock import MagicMock, patch

import pytest

from openclaw.agents.base import ActivityType
from openclaw.agents.bull_researcher import BullReport, BullResearcher
from openclaw.agents.trader import MarketAnalysis
from openclaw.core.economy import SurvivalStatus


class TestBullReport:
    """Test BullReport dataclass."""

    def test_default_creation(self):
        """Test creating BullReport with defaults."""
        report = BullReport(symbol="AAPL")

        assert report.symbol == "AAPL"
        assert report.bullish_factors == []
        assert report.counter_arguments == {}
        assert report.price_target == 0.0
        assert report.conviction_level == 0.5
        assert report.summary == ""
        assert report.risk_factors == []
        assert report.catalysts == []

    def test_full_creation(self):
        """Test creating BullReport with all fields."""
        report = BullReport(
            symbol="TSLA",
            bullish_factors=["Strong growth", "Market leadership"],
            counter_arguments={"Overvalued": "Growth justifies premium"},
            price_target=250.0,
            conviction_level=0.75,
            summary="Bull case for TSLA",
            risk_factors=["Competition", "Regulation"],
            catalysts=["Earnings beat", "New product launch"],
        )

        assert report.symbol == "TSLA"
        assert len(report.bullish_factors) == 2
        assert report.price_target == 250.0
        assert report.conviction_level == 0.75

    def test_conviction_bounds(self):
        """Test conviction level is bounded between 0 and 1."""
        report_high = BullReport(symbol="AAPL", conviction_level=1.5)
        assert report_high.conviction_level == 1.0

        report_low = BullReport(symbol="AAPL", conviction_level=-0.5)
        assert report_low.conviction_level == 0.0


class TestBullResearcherInitialization:
    """Test BullResearcher initialization."""

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

        assert agent.agent_id == "bull-1"
        assert agent.balance == 10000.0
        assert agent.skill_level == 0.5
        assert agent.decision_cost == 0.15
        assert agent._last_report is None
        assert agent._report_history == []

    def test_custom_initialization(self):
        """Test agent with custom parameters."""
        agent = BullResearcher(
            agent_id="bull-2",
            initial_capital=5000.0,
            skill_level=0.8,
        )

        assert agent.agent_id == "bull-2"
        assert agent.balance == 5000.0
        assert agent.skill_level == 0.8

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

        agent = BullResearcher(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 BullResearcher(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_stable_status_prefers_analysis(self, agent):
        """Test stable status leads to analysis/paper trade."""
        agent.economic_tracker.balance = 12000.0  # Stable

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

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

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

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

        assert result == ActivityType.ANALYZE


class TestAnalyze:
    """Test analyze method (async)."""

    @pytest.fixture
    def agent(self):
        """Create a test agent."""
        return BullResearcher(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 "bull_report" in result
        assert "cost" in result

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

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

        assert agent.balance == initial_balance - 0.15

    def test_analyze_stores_last_report(self, agent):
        """Test that analyze stores the report."""
        assert agent._last_report is None

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

        assert agent._last_report is not None
        assert agent._last_report.symbol == "TSLA"

    def test_analyze_adds_to_history(self, agent):
        """Test that analyze adds to report history."""
        assert len(agent._report_history) == 0

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

        assert len(agent._report_history) == 2


class TestGenerateBullCase:
    """Test generate_bull_case method."""

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

    def test_returns_bull_report(self, agent):
        """Test that generate_bull_case returns BullReport."""
        result = asyncio.run(agent.generate_bull_case("AAPL"))

        assert isinstance(result, BullReport)
        assert result.symbol == "AAPL"

    def test_deducts_decision_cost(self, agent):
        """Test that decision cost is deducted."""
        initial_balance = agent.balance

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

        assert agent.balance == initial_balance - 0.15

    def test_includes_bullish_factors(self, agent):
        """Test that bull report includes bullish factors."""
        result = asyncio.run(agent.generate_bull_case("AAPL"))

        assert isinstance(result.bullish_factors, list)
        assert len(result.bullish_factors) > 0

    def test_includes_counter_arguments(self, agent):
        """Test that bull report includes counter-arguments."""
        result = asyncio.run(agent.generate_bull_case("AAPL"))

        assert isinstance(result.counter_arguments, dict)
        assert len(result.counter_arguments) > 0

    def test_includes_price_target(self, agent):
        """Test that bull report includes price target."""
        result = asyncio.run(agent.generate_bull_case("AAPL"))

        assert isinstance(result.price_target, float)
        assert result.price_target >= 0

    def test_includes_conviction_level(self, agent):
        """Test that bull report includes conviction level."""
        result = asyncio.run(agent.generate_bull_case("AAPL"))

        assert 0.0 <= result.conviction_level <= 1.0

    def test_includes_summary(self, agent):
        """Test that bull report includes summary."""
        result = asyncio.run(agent.generate_bull_case("AAPL"))

        assert isinstance(result.summary, str)
        assert len(result.summary) > 0

    def test_includes_risk_factors(self, agent):
        """Test that bull report includes risk factors."""
        result = asyncio.run(agent.generate_bull_case("AAPL"))

        assert isinstance(result.risk_factors, list)
        assert len(result.risk_factors) > 0

    def test_includes_catalysts(self, agent):
        """Test that bull report includes catalysts."""
        result = asyncio.run(agent.generate_bull_case("AAPL"))

        assert isinstance(result.catalysts, list)
        assert len(result.catalysts) > 0

    def test_stores_report_in_history(self, agent):
        """Test that generated report is stored in history."""
        assert len(agent._report_history) == 0

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

        assert len(agent._report_history) == 1
        assert agent._last_report is not None


class TestExtractBullishFactors:
    """Test bullish factor extraction."""

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

    def test_extract_from_technical_dict(self, agent):
        """Test extracting factors from technical report (dict format)."""
        technical = {
            "trend": "uptrend",
            "indicators": {"rsi": 35.0, "macd": 0.5, "current_price": 100.0},
        }

        factors = agent._extract_bullish_factors(technical, None, None)

        assert any("uptrend" in f.lower() for f in factors)
        assert any("RSI" in f or "oversold" in f.lower() for f in factors)

    def test_extract_from_technical_object(self, agent):
        """Test extracting factors from technical report (object format)."""
        technical = MarketAnalysis(
            symbol="AAPL",
            trend="uptrend",
            volatility=0.2,
            volume_trend="increasing",
            support_level=90.0,
            resistance_level=110.0,
            indicators={"rsi": 35.0, "macd": 0.5, "current_price": 100.0},
        )

        factors = agent._extract_bullish_factors(technical, None, None)

        assert any("uptrend" in f.lower() for f in factors)

    def test_extract_from_sentiment_dict(self, agent):
        """Test extracting factors from sentiment report (dict format)."""
        sentiment = {"sentiment": "bullish", "score": 0.75}

        factors = agent._extract_bullish_factors(None, sentiment, None)

        assert any("sentiment" in f.lower() for f in factors)

    def test_extract_from_fundamental_dict(self, agent):
        """Test extracting factors from fundamental report (dict format)."""
        fundamental = {
            "valuation": "undervalued",
            "growth_rate": 0.25,
            "pe_ratio": 15.0,
        }

        factors = agent._extract_fundamental_bullish_factors(fundamental)

        assert any("undervalued" in f.lower() for f in factors)
        assert any("growth" in f.lower() for f in factors)

    def test_empty_reports_placeholder(self, agent):
        """Test placeholder factor when no reports provided."""
        factors = agent._extract_bullish_factors(None, None, None)

        assert len(factors) == 1
        assert "pending" in factors[0].lower()


class TestGenerateCounterArguments:
    """Test counter-argument generation."""

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

    def test_includes_common_counters(self, agent):
        """Test that common counter-arguments are included."""
        counters = agent._generate_counter_arguments(None, None, None)

        assert "Stock is overbought" in counters
        assert "Valuation is stretched" in counters
        assert "Recent rally is unsustainable" in counters
        assert "Market sentiment is too optimistic" in counters

    def test_counters_are_strings(self, agent):
        """Test that all counter-arguments are strings."""
        counters = agent._generate_counter_arguments(None, None, None)

        for key, value in counters.items():
            assert isinstance(key, str)
            assert isinstance(value, str)
            assert len(value) > 0


class TestCalculateConviction:
    """Test conviction calculation."""

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

    def test_base_conviction(self, agent):
        """Test base conviction level."""
        conviction = agent._calculate_conviction([], None, None, None)

        assert conviction >= 0.5  # Base conviction
        assert conviction <= 1.0

    def test_factors_boost_conviction(self, agent):
        """Test that more factors increase conviction."""
        low_factors = ["Factor 1"]
        high_factors = ["Factor 1", "Factor 2", "Factor 3", "Factor 4", "Factor 5"]

        low_conviction = agent._calculate_conviction(low_factors, None, None, None)
        high_conviction = agent._calculate_conviction(high_factors, None, None, None)

        assert high_conviction >= low_conviction

    def test_all_reports_max_conviction(self, agent):
        """Test that having all reports allows higher conviction."""
        factors = ["Factor 1", "Factor 2", "Factor 3"]

        partial_conviction = agent._calculate_conviction(factors, None, None, None)
        full_conviction = agent._calculate_conviction(
            factors, {"trend": "up"}, {"sentiment": "bullish"}, {"pe": 15}
        )

        # With all reports, max conviction is higher
        assert full_conviction >= partial_conviction


class TestPriceTarget:
    """Test price target generation."""

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

    def test_default_price(self, agent):
        """Test default price when no reports."""
        target = agent._generate_price_target("AAPL", None, None)

        # Default current price is 100, with 10-20% upside
        assert target >= 110.0
        assert target <= 130.0

    def test_price_from_technical(self, agent):
        """Test price target from technical report."""
        technical = {"indicators": {"current_price": 150.0}}

        target = agent._generate_price_target("AAPL", None, technical)

        # Target should be above current price
        assert target > 150.0

    def test_fundamental_boosts_target(self, agent):
        """Test that fundamental report adds upside."""
        technical = {"indicators": {"current_price": 100.0}}
        fundamental = {"pe_ratio": 15.0}

        target_without = agent._generate_price_target("AAPL", None, technical)
        target_with = agent._generate_price_target("AAPL", fundamental, technical)

        # With fundamental, target should be higher
        assert target_with > target_without


class TestIdentifyCatalysts:
    """Test catalyst identification."""

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

    def test_basic_catalysts(self, agent):
        """Test that basic catalysts are identified."""
        catalysts = agent._identify_catalysts(None, None, None)

        assert len(catalysts) >= 4
        assert any("Earnings" in c for c in catalysts)
        assert any("Institutional" in c for c in catalysts)

    def test_high_skill_extra_catalyst(self, agent):
        """Test that high skill adds extra catalysts."""
        agent.state.skill_level = 0.8

        catalysts = agent._identify_catalysts(None, None, None)

        assert any("sector" in c.lower() for c in catalysts)


class TestIdentifyRisks:
    """Test risk identification."""

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

    def test_basic_risks(self, agent):
        """Test that basic risks are identified."""
        risks = agent._identify_risks(None, None, None)

        assert len(risks) >= 3
        assert any("market" in r.lower() for r in risks)
        assert any("earnings" in r.lower() for r in risks)

    def test_high_skill_extra_risk(self, agent):
        """Test that high skill adds extra risks."""
        agent.state.skill_level = 0.7

        risks = agent._identify_risks(None, None, None)

        assert any("regulatory" in r.lower() for r in risks)


class TestGetLastReport:
    """Test get_last_report method."""

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

    def test_no_report_returns_none(self, agent):
        """Test that None is returned when no reports."""
        result = agent.get_last_report()

        assert result is None

    def test_returns_last_report(self, agent):
        """Test that last report is returned."""
        asyncio.run(agent.generate_bull_case("AAPL"))

        result = agent.get_last_report()

        assert result is not None
        assert result.symbol == "AAPL"


class TestGetReportHistory:
    """Test get_report_history method."""

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

    def test_empty_history(self, agent):
        """Test empty history."""
        history = agent.get_report_history()

        assert history == []

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

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

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

    def test_multiple_reports(self, agent):
        """Test history with multiple reports."""
        asyncio.run(agent.generate_bull_case("AAPL"))
        asyncio.run(agent.generate_bull_case("TSLA"))
        asyncio.run(agent.generate_bull_case("NVDA"))

        history = agent.get_report_history()

        assert len(history) == 3


class TestGetBullishRecommendation:
    """Test get_bullish_recommendation method."""

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

    def test_no_analysis_returns_hold(self, agent):
        """Test HOLD recommendation when no analysis."""
        result = agent.get_bullish_recommendation("AAPL")

        assert result["symbol"] == "AAPL"
        assert result["recommendation"] == "HOLD"
        assert result["conviction"] == 0.0

    def test_strong_buy_for_high_conviction(self, agent):
        """Test STRONG_BUY for high conviction."""
        # Create a high conviction report
        report = BullReport(
            symbol="AAPL",
            conviction_level=0.8,
            bullish_factors=["F1", "F2", "F3"],
            price_target=200.0,
        )
        agent._last_report = report

        result = agent.get_bullish_recommendation("AAPL")

        assert result["recommendation"] == "STRONG_BUY"

    def test_buy_for_moderate_conviction(self, agent):
        """Test BUY for moderate conviction."""
        report = BullReport(
            symbol="AAPL",
            conviction_level=0.65,
            bullish_factors=["F1", "F2"],
            price_target=150.0,
        )
        agent._last_report = report

        result = agent.get_bullish_recommendation("AAPL")

        assert result["recommendation"] == "BUY"

    def test_accumulate_for_low_conviction(self, agent):
        """Test ACCUMULATE for lower conviction."""
        report = BullReport(
            symbol="AAPL",
            conviction_level=0.5,
            bullish_factors=["F1"],
            price_target=120.0,
        )
        agent._last_report = report

        result = agent.get_bullish_recommendation("AAPL")

        assert result["recommendation"] == "ACCUMULATE"

    def test_different_symbol_returns_hold(self, agent):
        """Test HOLD when asking for different symbol than last analyzed."""
        report = BullReport(symbol="AAPL", conviction_level=0.8)
        agent._last_report = report

        result = agent.get_bullish_recommendation("TSLA")

        assert result["recommendation"] == "HOLD"


class TestDecisionCost:
    """Test decision cost deduction."""

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

    def test_decision_cost_constant(self):
        """Test that decision cost is $0.15."""
        agent = BullResearcher(agent_id="test", initial_capital=10000.0)

        assert agent.decision_cost == 0.15

    def test_analyze_deducts_fixed_cost(self, agent):
        """Test that analyze deducts exactly $0.15."""
        initial_balance = agent.balance

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

        assert agent.balance == initial_balance - 0.15

    def test_generate_bull_case_deducts_fixed_cost(self, agent):
        """Test that generate_bull_case deducts exactly $0.15."""
        initial_balance = agent.balance

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

        assert agent.balance == initial_balance - 0.15

    def test_multiple_calls_deduct_multiple_times(self, agent):
        """Test that each call deducts cost."""
        initial_balance = agent.balance

        asyncio.run(agent.generate_bull_case("AAPL"))
        asyncio.run(agent.generate_bull_case("TSLA"))
        asyncio.run(agent.generate_bull_case("NVDA"))

        expected_balance = initial_balance - (0.15 * 3)
        assert agent.balance == expected_balance


class TestSkillLevelImpact:
    """Test impact of skill level on analysis."""

    def test_high_skill_higher_conviction(self):
        """Test that high skill produces higher conviction."""
        low_skill = BullResearcher(agent_id="low", initial_capital=10000.0, skill_level=0.3)
        high_skill = BullResearcher(agent_id="high", initial_capital=10000.0, skill_level=0.9)

        low_report = asyncio.run(low_skill.generate_bull_case("AAPL"))
        high_report = asyncio.run(high_skill.generate_bull_case("AAPL"))

        assert high_report.conviction_level >= low_report.conviction_level

    def test_high_skill_more_catalysts(self):
        """Test that high skill identifies more catalysts."""
        low_skill = BullResearcher(agent_id="low", initial_capital=10000.0, skill_level=0.3)
        high_skill = BullResearcher(agent_id="high", initial_capital=10000.0, skill_level=0.9)

        low_catalysts = low_skill._identify_catalysts(None, None, None)
        high_catalysts = high_skill._identify_catalysts(None, None, None)

        assert len(high_catalysts) >= len(low_catalysts)

    def test_high_skill_more_risks(self):
        """Test that high skill identifies more risks."""
        low_skill = BullResearcher(agent_id="low", initial_capital=10000.0, skill_level=0.3)
        high_skill = BullResearcher(agent_id="high", initial_capital=10000.0, skill_level=0.7)

        low_risks = low_skill._identify_risks(None, None, None)
        high_risks = high_skill._identify_risks(None, None, None)

        assert len(high_risks) >= len(low_risks)
