"""Unit tests for MarketAnalyst agent.

This module tests the MarketAnalyst class including technical indicator
calculations, trend identification, and signal generation.
"""

import asyncio

import numpy as np
import pandas as pd
import pytest

from openclaw.agents.base import ActivityType
from openclaw.agents.market_analyst import MarketAnalyst, TechnicalReport
from openclaw.core.economy import SurvivalStatus


class TestMarketAnalystInitialization:
    """Test MarketAnalyst initialization."""

    def test_default_initialization(self):
        """Test agent with default parameters."""
        agent = MarketAnalyst(agent_id="analyst-1", initial_capital=1000.0)

        assert agent.agent_id == "analyst-1"
        assert agent.balance == 1000.0
        assert agent.skill_level == 0.5
        assert agent.decision_cost == 0.05
        assert agent._last_report is None

    def test_custom_initialization(self):
        """Test agent with custom parameters."""
        agent = MarketAnalyst(
            agent_id="analyst-2",
            initial_capital=500.0,
            skill_level=0.8,
        )

        assert agent.agent_id == "analyst-2"
        assert agent.balance == 500.0
        assert agent.skill_level == 0.8

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

        agent = MarketAnalyst(agent_id="test", initial_capital=1000.0)

        assert isinstance(agent, BaseAgent)


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

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

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

        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 = 350.0
        agent.state.skill_level = 0.5

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

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

    def test_stable_status_prefers_analysis(self, agent):
        """Test stable status leads to analysis."""
        agent.economic_tracker.balance = 2000.0

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

        assert result == ActivityType.ANALYZE


class TestAnalyze:
    """Test analyze method."""

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

    @pytest.fixture
    def sample_data(self):
        """Create sample price data."""
        np.random.seed(42)
        n_periods = 100
        returns = np.random.normal(0.001, 0.02, n_periods)
        prices = 100 * np.exp(np.cumsum(returns))

        return pd.DataFrame(
            {
                "open": prices * (1 + np.random.normal(0, 0.001, n_periods)),
                "high": prices * (1 + abs(np.random.normal(0, 0.01, n_periods))),
                "low": prices * (1 - abs(np.random.normal(0, 0.01, n_periods))),
                "close": prices,
                "volume": np.random.randint(1000000, 10000000, n_periods),
            }
        )

    def test_analyze_returns_technical_report(self, agent, sample_data):
        """Test that analyze returns a TechnicalReport."""
        result = asyncio.run(agent.analyze("AAPL", sample_data))

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

    def test_analyze_deducts_decision_cost(self, agent, sample_data):
        """Test that analyze deducts the $0.05 decision cost."""
        initial_balance = agent.balance

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

        assert agent.balance == initial_balance - 0.05

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

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

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

    def test_analyze_without_data_uses_sample(self, agent):
        """Test that analyze generates sample data if none provided."""
        result = asyncio.run(agent.analyze("AAPL"))

        assert isinstance(result, TechnicalReport)
        assert result.symbol == "AAPL"
        assert len(result.indicators) > 0

    def test_get_last_report_returns_report(self, agent, sample_data):
        """Test get_last_report method."""
        assert agent.get_last_report() is None

        asyncio.run(agent.analyze("NVDA", sample_data))

        report = agent.get_last_report()
        assert report is not None
        assert report.symbol == "NVDA"


class TestCalculateIndicators:
    """Test technical indicator calculations."""

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

    @pytest.fixture
    def sample_data(self):
        """Create sample price data."""
        np.random.seed(42)
        n_periods = 100
        returns = np.random.normal(0.001, 0.02, n_periods)
        prices = 100 * np.exp(np.cumsum(returns))

        return pd.DataFrame(
            {
                "open": prices * (1 + np.random.normal(0, 0.001, n_periods)),
                "high": prices * (1 + abs(np.random.normal(0, 0.01, n_periods))),
                "low": prices * (1 - abs(np.random.normal(0, 0.01, n_periods))),
                "close": prices,
                "volume": np.random.randint(1000000, 10000000, n_periods),
            }
        )

    def test_indicators_structure(self, agent, sample_data):
        """Test that all expected indicators are present."""
        indicators = agent._calculate_indicators(sample_data)

        expected_keys = [
            "current_price",
            "ma_20",
            "ma_50",
            "ema_12",
            "ema_26",
            "rsi",
            "macd",
            "macd_signal",
            "macd_histogram",
            "bb_upper",
            "bb_middle",
            "bb_lower",
        ]

        for key in expected_keys:
            assert key in indicators

    def test_current_price_present(self, agent, sample_data):
        """Test that current price is calculated."""
        indicators = agent._calculate_indicators(sample_data)

        assert indicators["current_price"] is not None
        assert isinstance(indicators["current_price"], (int, float))
        assert indicators["current_price"] > 0

    def test_rsi_in_valid_range(self, agent, sample_data):
        """Test that RSI is within 0-100 range."""
        indicators = agent._calculate_indicators(sample_data)

        rsi = indicators.get("rsi")
        if rsi is not None:
            assert 0 <= rsi <= 100

    def test_bollinger_bands_relationship(self, agent, sample_data):
        """Test Bollinger Bands mathematical relationships."""
        indicators = agent._calculate_indicators(sample_data)

        upper = indicators.get("bb_upper")
        middle = indicators.get("bb_middle")
        lower = indicators.get("bb_lower")

        if all(v is not None for v in [upper, middle, lower]):
            assert upper >= middle
            assert middle >= lower
            assert upper > lower


class TestIdentifyTrend:
    """Test trend identification."""

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

    def test_identify_uptrend(self, agent):
        """Test uptrend identification."""
        # Create uptrend data with enough periods for 50-period MA
        np.random.seed(42)
        n_periods = 60
        trend = np.linspace(100, 150, n_periods)  # Upward trend
        noise = np.random.normal(0, 0.5, n_periods)
        prices = trend + noise

        data = pd.DataFrame(
            {
                "open": prices * 0.99,
                "high": prices * 1.01,
                "low": prices * 0.98,
                "close": prices,
                "volume": [1000000] * n_periods,
            }
        )

        indicators = agent._calculate_indicators(data)
        trend = agent._identify_trend(data, indicators)

        assert trend in ["uptrend", "sideways"]

    def test_identify_downtrend(self, agent):
        """Test downtrend identification."""
        # Create downtrend data with enough periods for 50-period MA
        np.random.seed(42)
        n_periods = 60
        trend = np.linspace(150, 100, n_periods)  # Downward trend
        noise = np.random.normal(0, 0.5, n_periods)
        prices = trend + noise

        data = pd.DataFrame(
            {
                "open": prices * 0.99,
                "high": prices * 1.01,
                "low": prices * 0.98,
                "close": prices,
                "volume": [1000000] * n_periods,
            }
        )

        indicators = agent._calculate_indicators(data)
        trend = agent._identify_trend(data, indicators)

        assert trend in ["downtrend", "sideways"]

    def test_trend_returned_in_report(self, agent):
        """Test that trend is included in analysis report."""
        result = asyncio.run(agent.analyze("AAPL"))

        assert result.trend in ["uptrend", "downtrend", "sideways", "insufficient_data"]


class TestGenerateSignals:
    """Test signal generation."""

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

    def test_signals_structure(self, agent):
        """Test that all expected signals are present."""
        indicators = {
            "rsi": 50.0,
            "macd": 0.0,
            "macd_signal": 0.0,
            "current_price": 100.0,
            "bb_upper": 110.0,
            "bb_lower": 90.0,
        }

        signals = agent._generate_signals(indicators)

        expected_keys = ["overall", "rsi_signal", "macd_signal", "bb_signal"]
        for key in expected_keys:
            assert key in signals

    def test_oversold_rsi_generates_buy_signal(self, agent):
        """Test that oversold RSI generates buy signal."""
        indicators = {
            "rsi": 25.0,
            "macd": 0.5,
            "macd_signal": 0.0,
            "current_price": 100.0,
            "bb_upper": 110.0,
            "bb_lower": 90.0,
        }

        signals = agent._generate_signals(indicators)

        assert signals["rsi_signal"] == "buy"

    def test_overbought_rsi_generates_sell_signal(self, agent):
        """Test that overbought RSI generates sell signal."""
        indicators = {
            "rsi": 75.0,
            "macd": -0.5,
            "macd_signal": 0.0,
            "current_price": 100.0,
            "bb_upper": 110.0,
            "bb_lower": 90.0,
        }

        signals = agent._generate_signals(indicators)

        assert signals["rsi_signal"] == "sell"

    def test_macd_bullish_generates_buy_signal(self, agent):
        """Test that MACD above signal generates buy signal."""
        indicators = {
            "rsi": 50.0,
            "macd": 0.5,
            "macd_signal": 0.0,
            "current_price": 100.0,
            "bb_upper": 110.0,
            "bb_lower": 90.0,
        }

        signals = agent._generate_signals(indicators)

        assert signals["macd_signal"] == "buy"

    def test_macd_bearish_generates_sell_signal(self, agent):
        """Test that MACD below signal generates sell signal."""
        indicators = {
            "rsi": 50.0,
            "macd": -0.5,
            "macd_signal": 0.0,
            "current_price": 100.0,
            "bb_upper": 110.0,
            "bb_lower": 90.0,
        }

        signals = agent._generate_signals(indicators)

        assert signals["macd_signal"] == "sell"

    def test_bb_price_above_upper_generates_sell(self, agent):
        """Test price above upper Bollinger Band generates sell signal."""
        indicators = {
            "rsi": 50.0,
            "macd": 0.0,
            "macd_signal": 0.0,
            "current_price": 115.0,
            "bb_upper": 110.0,
            "bb_lower": 90.0,
        }

        signals = agent._generate_signals(indicators)

        assert signals["bb_signal"] == "sell"

    def test_bb_price_below_lower_generates_buy(self, agent):
        """Test price below lower Bollinger Band generates buy signal."""
        indicators = {
            "rsi": 50.0,
            "macd": 0.0,
            "macd_signal": 0.0,
            "current_price": 85.0,
            "bb_upper": 110.0,
            "bb_lower": 90.0,
        }

        signals = agent._generate_signals(indicators)

        assert signals["bb_signal"] == "buy"

    def test_consensus_buy_signal(self, agent):
        """Test overall buy signal when multiple indicators agree."""
        indicators = {
            "rsi": 25.0,
            "macd": 0.5,
            "macd_signal": 0.0,
            "current_price": 85.0,
            "bb_upper": 110.0,
            "bb_lower": 90.0,
        }

        signals = agent._generate_signals(indicators)

        assert signals["overall"] == "buy"

    def test_consensus_sell_signal(self, agent):
        """Test overall sell signal when multiple indicators agree."""
        indicators = {
            "rsi": 75.0,
            "macd": -0.5,
            "macd_signal": 0.0,
            "current_price": 115.0,
            "bb_upper": 110.0,
            "bb_lower": 90.0,
        }

        signals = agent._generate_signals(indicators)

        assert signals["overall"] == "sell"


class TestCalculateConfidence:
    """Test confidence calculation."""

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

    def test_confidence_within_range(self, agent):
        """Test that confidence is within 0-1 range."""
        indicators = {"rsi": 50.0}
        signals = {"overall": "neutral"}

        confidence = agent._calculate_confidence(indicators, signals)

        assert 0.0 <= confidence <= 1.0

    def test_high_skill_increases_confidence(self):
        """Test that high skill level increases confidence."""
        low_skill_agent = MarketAnalyst(agent_id="low", initial_capital=1000.0, skill_level=0.3)
        high_skill_agent = MarketAnalyst(agent_id="high", initial_capital=1000.0, skill_level=0.9)

        indicators = {"rsi": 70.0}
        signals = {"overall": "sell", "rsi_signal": "sell", "macd_signal": "sell", "bb_signal": "neutral"}

        low_confidence = low_skill_agent._calculate_confidence(indicators, signals)
        high_confidence = high_skill_agent._calculate_confidence(indicators, signals)

        assert high_confidence >= low_confidence

    def test_extreme_rsi_increases_confidence(self, agent):
        """Test that extreme RSI values increase confidence."""
        neutral_indicators = {"rsi": 50.0}
        extreme_indicators = {"rsi": 90.0}
        signals = {"overall": "neutral"}

        neutral_confidence = agent._calculate_confidence(neutral_indicators, signals)
        extreme_confidence = agent._calculate_confidence(extreme_indicators, signals)

        assert extreme_confidence > neutral_confidence


class TestTechnicalReport:
    """Test TechnicalReport dataclass."""

    def test_report_creation(self):
        """Test creating a TechnicalReport."""
        report = TechnicalReport(
            symbol="AAPL",
            trend="uptrend",
            indicators={"rsi": 50.0, "macd": 0.5},
            signals={"overall": "buy", "rsi_signal": "buy"},
            confidence=0.75,
        )

        assert report.symbol == "AAPL"
        assert report.trend == "uptrend"
        assert report.indicators["rsi"] == 50.0
        assert report.signals["overall"] == "buy"
        assert report.confidence == 0.75


class TestIntegration:
    """Integration tests for MarketAnalyst."""

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

    def test_full_analysis_workflow(self, agent):
        """Test the complete analysis workflow."""
        # Create trending data
        np.random.seed(42)
        n_periods = 100
        trend = np.linspace(0, 20, n_periods)
        noise = np.random.normal(0, 1, n_periods)
        prices = 100 + trend + np.cumsum(noise * 0.1)

        data = pd.DataFrame(
            {
                "open": prices * 0.99,
                "high": prices * 1.01,
                "low": prices * 0.98,
                "close": prices,
                "volume": np.random.randint(1000000, 10000000, n_periods),
            }
        )

        result = asyncio.run(agent.analyze("AAPL", data))

        assert isinstance(result, TechnicalReport)
        assert result.symbol == "AAPL"
        assert result.trend in ["uptrend", "downtrend", "sideways", "insufficient_data"]
        assert len(result.indicators) > 0
        assert len(result.signals) > 0
        assert 0.0 <= result.confidence <= 1.0

        # Verify cost was deducted
        assert agent.balance == 1000.0 - 0.05

    def test_multiple_analyses_accumulate_costs(self, agent):
        """Test that multiple analyses accumulate decision costs."""
        initial_balance = agent.balance

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

        expected_balance = initial_balance - (0.05 * 3)
        assert agent.balance == pytest.approx(expected_balance, rel=1e-9)
