"""Unit tests for DecisionCostCalculator."""

import pytest

from openclaw.core.config import CostStructure
from openclaw.core.costs import DecisionCostBreakdown, DecisionCostCalculator


class TestDecisionCostCalculatorInitialization:
    """Test calculator initialization."""

    def test_default_initialization(self):
        """Test calculator with default parameters."""
        calc = DecisionCostCalculator()

        assert calc.llm_input_per_1m == 2.5
        assert calc.llm_output_per_1m == 10.0
        assert calc.market_data_per_call == 0.01

    def test_custom_initialization(self):
        """Test calculator with custom parameters."""
        calc = DecisionCostCalculator(
            llm_input_per_1m=3.0,
            llm_output_per_1m=12.0,
            market_data_per_call=0.02,
        )

        assert calc.llm_input_per_1m == 3.0
        assert calc.llm_output_per_1m == 12.0
        assert calc.market_data_per_call == 0.02

    def test_from_config(self):
        """Test creating calculator from CostStructure config."""
        config = CostStructure(
            llm_input_per_1m=5.0,
            llm_output_per_1m=15.0,
            market_data_per_call=0.05,
            trade_fee_rate=0.002,
        )

        calc = DecisionCostCalculator.from_config(config)

        assert calc.llm_input_per_1m == 5.0
        assert calc.llm_output_per_1m == 15.0
        assert calc.market_data_per_call == 0.05


class TestCalculateDecisionCost:
    """Test decision cost calculation."""

    def test_token_cost_calculation(self):
        """Test LLM token cost calculation."""
        calc = DecisionCostCalculator()

        # 1000 input tokens, 500 output tokens, 0 data calls
        cost = calc.calculate_decision_cost(
            tokens_input=1000, tokens_output=500, market_data_calls=0
        )

        # Expected: (1000/1e6 * 2.5) + (500/1e6 * 10.0) = 0.0025 + 0.005 = 0.0075
        expected_cost = round(1000 / 1e6 * 2.5 + 500 / 1e6 * 10.0, 4)
        assert cost == expected_cost

    def test_market_data_cost(self):
        """Test market data API call cost."""
        calc = DecisionCostCalculator(market_data_per_call=0.01)

        cost = calc.calculate_decision_cost(
            tokens_input=0, tokens_output=0, market_data_calls=5
        )

        # Expected: 5 * 0.01 = 0.05
        assert cost == 0.05

    def test_combined_costs(self):
        """Test combined token and data costs."""
        calc = DecisionCostCalculator()

        cost = calc.calculate_decision_cost(
            tokens_input=1000000,  # 1M tokens
            tokens_output=500000,  # 500K tokens
            market_data_calls=10,
        )

        # Expected: (1.0 * 2.5) + (0.5 * 10.0) + (10 * 0.01) = 2.5 + 5.0 + 0.1 = 7.6
        expected_cost = round(2.5 + 5.0 + 0.1, 4)
        assert cost == expected_cost

    def test_precision_to_four_decimals(self):
        """Test that costs are calculated with 4 decimal precision."""
        calc = DecisionCostCalculator()

        cost = calc.calculate_decision_cost(
            tokens_input=333333, tokens_output=333333, market_data_calls=3
        )

        # Should be rounded to 4 decimal places
        assert len(str(cost).split(".")[-1]) <= 4

    def test_zero_values(self):
        """Test calculation with all zero values."""
        calc = DecisionCostCalculator()

        cost = calc.calculate_decision_cost(
            tokens_input=0, tokens_output=0, market_data_calls=0
        )

        assert cost == 0.0

    def test_large_token_counts(self):
        """Test calculation with large token counts."""
        calc = DecisionCostCalculator()

        cost = calc.calculate_decision_cost(
            tokens_input=10000000,  # 10M tokens
            tokens_output=5000000,  # 5M tokens
            market_data_calls=100,
        )

        # Expected: (10 * 2.5) + (5 * 10.0) + (100 * 0.01) = 25 + 50 + 1 = 76
        assert cost == 76.0

    def test_no_side_effects(self):
        """Test that calculator has no side effects (pure function)."""
        calc = DecisionCostCalculator()

        # Call multiple times with same inputs
        cost1 = calc.calculate_decision_cost(
            tokens_input=1000, tokens_output=500, market_data_calls=2
        )
        cost2 = calc.calculate_decision_cost(
            tokens_input=1000, tokens_output=500, market_data_calls=2
        )
        cost3 = calc.calculate_decision_cost(
            tokens_input=1000, tokens_output=500, market_data_calls=2
        )

        # All should return the same value
        assert cost1 == cost2 == cost3


class TestCalculateDetailed:
    """Test detailed cost breakdown."""

    def test_detailed_breakdown_structure(self):
        """Test that detailed breakdown returns correct structure."""
        calc = DecisionCostCalculator()

        breakdown = calc.calculate_detailed(
            tokens_input=1000,
            tokens_output=500,
            market_data_calls=2,
        )

        assert isinstance(breakdown, DecisionCostBreakdown)
        assert breakdown.input_tokens == 1000
        assert breakdown.output_tokens == 500
        assert breakdown.market_data_calls == 2

    def test_detailed_cost_calculation(self):
        """Test that detailed breakdown calculates costs correctly."""
        calc = DecisionCostCalculator()

        breakdown = calc.calculate_detailed(
            tokens_input=1000000,  # 1M input tokens
            tokens_output=500000,  # 500K output tokens
            market_data_calls=10,
        )

        # Expected input cost: 1.0 * 2.5 = 2.5
        assert breakdown.input_cost == 2.5

        # Expected output cost: 0.5 * 10.0 = 5.0
        assert breakdown.output_cost == 5.0

        # Expected data cost: 10 * 0.01 = 0.1
        assert breakdown.data_cost == 0.1

        # Expected total: 2.5 + 5.0 + 0.1 = 7.6
        assert breakdown.total_cost == 7.6

    def test_detailed_matches_simple_calculation(self):
        """Test that detailed calculation matches simple calculation."""
        calc = DecisionCostCalculator()

        simple_cost = calc.calculate_decision_cost(
            tokens_input=1000, tokens_output=500, market_data_calls=3
        )

        detailed = calc.calculate_detailed(
            tokens_input=1000, tokens_output=500, market_data_calls=3
        )

        assert simple_cost == detailed.total_cost


class TestPrivateMethods:
    """Test private helper methods."""

    def test_calculate_input_token_cost(self):
        """Test input token cost calculation."""
        calc = DecisionCostCalculator(llm_input_per_1m=2.5)

        cost = calc._calculate_input_token_cost(1000000)

        assert cost == 2.5

    def test_calculate_output_token_cost(self):
        """Test output token cost calculation."""
        calc = DecisionCostCalculator(llm_output_per_1m=10.0)

        cost = calc._calculate_output_token_cost(500000)

        assert cost == 5.0

    def test_calculate_data_cost(self):
        """Test market data cost calculation."""
        calc = DecisionCostCalculator(market_data_per_call=0.01)

        cost = calc._calculate_data_cost(10)

        assert cost == 0.1


class TestCalculatorComparisonWithEconomicTracker:
    """Test that calculator matches EconomicTracker calculations."""

    def test_costs_match_tracker(self):
        """Test that calculator produces same costs as TradingEconomicTracker."""
        from openclaw.core.economy import TradingEconomicTracker

        # Create calculator and tracker with same rates
        calc = DecisionCostCalculator(
            llm_input_per_1m=2.5,
            llm_output_per_1m=10.0,
            market_data_per_call=0.01,
        )

        tracker = TradingEconomicTracker(
            agent_id="test",
            token_cost_per_1m_input=2.5,
            token_cost_per_1m_output=10.0,
            data_cost_per_call=0.01,
        )

        # Calculate costs
        calc_cost = calc.calculate_decision_cost(
            tokens_input=1000, tokens_output=500, market_data_calls=2
        )

        initial_balance = tracker.balance
        tracker_cost = tracker.calculate_decision_cost(
            tokens_input=1000, tokens_output=500, market_data_calls=2
        )

        # Costs should match
        assert calc_cost == tracker_cost

        # Calculator has no side effects
        # Tracker has side effects (balance changed)
        assert tracker.balance < initial_balance


class TestEdgeCases:
    """Test edge cases and error conditions."""

    def test_very_small_token_counts(self):
        """Test with very small token counts."""
        calc = DecisionCostCalculator()

        cost = calc.calculate_decision_cost(
            tokens_input=1, tokens_output=1, market_data_calls=1
        )

        # Expected: (1/1e6 * 2.5) + (1/1e6 * 10.0) + 0.01 = 0.0000025 + 0.00001 + 0.01
        # After rounding to 4 decimals: 0.01
        assert cost == 0.01  # Data cost dominates, token costs rounded away

    def test_repr(self):
        """Test string representation."""
        calc = DecisionCostCalculator()

        repr_str = repr(calc)

        assert "DecisionCostCalculator" in repr_str
        assert "2.5" in repr_str
        assert "10.0" in repr_str
        assert "0.01" in repr_str


class TestPydanticModels:
    """Test Pydantic model validation."""

    def test_decision_cost_breakdown_validation(self):
        """Test DecisionCostBreakdown validation."""
        breakdown = DecisionCostBreakdown(
            input_tokens=1000,
            output_tokens=500,
            market_data_calls=2,
            input_cost=0.0025,
            output_cost=0.005,
            data_cost=0.02,
            total_cost=0.0275,
        )

        assert breakdown.input_tokens == 1000
        assert breakdown.total_cost == 0.0275

    def test_decision_cost_breakdown_zero_values(self):
        """Test DecisionCostBreakdown with zero values."""
        breakdown = DecisionCostBreakdown(
            input_tokens=0,
            output_tokens=0,
            market_data_calls=0,
            input_cost=0.0,
            output_cost=0.0,
            data_cost=0.0,
            total_cost=0.0,
        )

        assert breakdown.total_cost == 0.0

    def test_decision_cost_breakdown_negative_validation(self):
        """Test that negative values are rejected."""
        with pytest.raises(ValueError):
            DecisionCostBreakdown(
                input_tokens=-1,  # Negative should fail
                output_tokens=500,
                market_data_calls=2,
                input_cost=0.0025,
                output_cost=0.005,
                data_cost=0.02,
                total_cost=0.0275,
            )
