"""
Tests for Dynamic Pricing and Storage Tracking

Tests cover:
- DynamicPricingEngine
- StorageTracker
- Surge pricing tiers
- Time-of-day adjustments
- GPU and region multipliers
"""

from decimal import Decimal

from verlex_server.gateway.backend.billing import (
    DEMAND_THRESHOLDS,
    GPU_DEMAND_MULTIPLIERS,
    SURGE_PRICING_TIERS,
    TIME_MULTIPLIERS,
    DemandMetrics,
    DynamicPricingEngine,
    StorageTracker,
    StorageUsage,
)


class TestDynamicPricingEngine:
    """Tests for the DynamicPricingEngine class."""

    def setup_method(self):
        """Set up test fixtures."""
        self.engine = DynamicPricingEngine()

    def test_surge_pricing_tiers_defined(self):
        """Test that all surge tiers are defined correctly."""
        assert SURGE_PRICING_TIERS["none"] == Decimal("1.00")
        assert SURGE_PRICING_TIERS["low"] == Decimal("1.10")
        assert SURGE_PRICING_TIERS["medium"] == Decimal("1.25")
        assert SURGE_PRICING_TIERS["high"] == Decimal("1.50")
        assert SURGE_PRICING_TIERS["extreme"] == Decimal("2.00")

    def test_demand_thresholds_defined(self):
        """Test that demand thresholds are defined correctly."""
        assert DEMAND_THRESHOLDS["none"] == 0.50
        assert DEMAND_THRESHOLDS["low"] == 0.65
        assert DEMAND_THRESHOLDS["medium"] == 0.80
        assert DEMAND_THRESHOLDS["high"] == 0.90
        assert DEMAND_THRESHOLDS["extreme"] == 0.95

    def test_time_multipliers_defined(self):
        """Test that time multipliers are defined correctly."""
        assert TIME_MULTIPLIERS["peak"] == Decimal("1.15")
        assert TIME_MULTIPLIERS["standard"] == Decimal("1.00")
        assert TIME_MULTIPLIERS["off_peak"] == Decimal("0.90")

    def test_gpu_demand_multipliers_defined(self):
        """Test that GPU demand multipliers are built from catalog."""
        # GPU_DEMAND_MULTIPLIERS is dynamic — built from catalog at import time.
        # In test env (no cloud API), it may be empty (all GPUs get 1.00 default).
        # When catalog is available, it maps GPU names to Decimal multipliers.
        assert isinstance(GPU_DEMAND_MULTIPLIERS, dict)
        for gpu, mult in GPU_DEMAND_MULTIPLIERS.items():
            assert isinstance(gpu, str)
            assert isinstance(mult, Decimal)
            assert Decimal("0.90") <= mult <= Decimal("1.25")

    def test_get_time_of_day_tier_peak(self):
        """Test peak hours detection (14:00-22:00 UTC)."""
        assert self.engine.get_time_of_day_tier(14) == "peak"
        assert self.engine.get_time_of_day_tier(18) == "peak"
        assert self.engine.get_time_of_day_tier(21) == "peak"

    def test_get_time_of_day_tier_standard(self):
        """Test standard hours detection (06:00-14:00 UTC)."""
        assert self.engine.get_time_of_day_tier(6) == "standard"
        assert self.engine.get_time_of_day_tier(10) == "standard"
        assert self.engine.get_time_of_day_tier(13) == "standard"

    def test_get_time_of_day_tier_off_peak(self):
        """Test off-peak hours detection (22:00-06:00 UTC)."""
        assert self.engine.get_time_of_day_tier(22) == "off_peak"
        assert self.engine.get_time_of_day_tier(0) == "off_peak"
        assert self.engine.get_time_of_day_tier(5) == "off_peak"

    def test_get_time_multiplier(self):
        """Test time-based multiplier retrieval."""
        assert self.engine.get_time_multiplier(15) == Decimal("1.15")  # Peak
        assert self.engine.get_time_multiplier(10) == Decimal("1.00")  # Standard
        assert self.engine.get_time_multiplier(2) == Decimal("0.90")   # Off-peak

    def test_get_gpu_multiplier(self):
        """Test GPU-specific multiplier retrieval."""
        # Dynamic multipliers from catalog — unknown GPUs always get 1.00
        assert self.engine.get_gpu_multiplier(None) == Decimal("1.00")
        assert self.engine.get_gpu_multiplier("unknown") == Decimal("1.00")
        # Known GPUs get their catalog-derived multiplier (or 1.00 if catalog empty)
        for gpu_type in ["H100", "A100", "T4"]:
            mult = self.engine.get_gpu_multiplier(gpu_type)
            assert isinstance(mult, Decimal)
            assert Decimal("0.90") <= mult <= Decimal("1.25")

    def test_get_region_multiplier(self):
        """Test region scarcity multiplier retrieval."""
        assert self.engine.get_region_multiplier("us-east-1") == Decimal("1.00")
        assert self.engine.get_region_multiplier("asia-east1") == Decimal("1.10")
        assert self.engine.get_region_multiplier("unknown") == Decimal("1.00")

    def test_demand_metrics_get_surge_tier(self):
        """Test surge tier determination from capacity utilization."""
        # No surge (< 65%)
        metrics = DemandMetrics(capacity_utilization=0.4)
        assert metrics.get_surge_tier() == "none"

        metrics = DemandMetrics(capacity_utilization=0.55)
        assert metrics.get_surge_tier() == "none"

        # Low surge (65-80%)
        metrics = DemandMetrics(capacity_utilization=0.65)
        assert metrics.get_surge_tier() == "low"

        metrics = DemandMetrics(capacity_utilization=0.75)
        assert metrics.get_surge_tier() == "low"

        # Medium surge (80-90%)
        metrics = DemandMetrics(capacity_utilization=0.80)
        assert metrics.get_surge_tier() == "medium"

        metrics = DemandMetrics(capacity_utilization=0.85)
        assert metrics.get_surge_tier() == "medium"

        # High surge (90-95%)
        metrics = DemandMetrics(capacity_utilization=0.90)
        assert metrics.get_surge_tier() == "high"

        # Extreme surge (95%+)
        metrics = DemandMetrics(capacity_utilization=0.97)
        assert metrics.get_surge_tier() == "extreme"

    def test_update_demand_metrics(self):
        """Test updating demand metrics cache."""
        metrics = self.engine.update_demand_metrics(
            provider="aws",
            region="us-east-1",
            gpu_type="A100",
            capacity_utilization=0.85,
            queue_depth=10,
            active_jobs=50,
            available_instances=20,
        )

        assert metrics.capacity_utilization == 0.85
        assert metrics.queue_depth == 10
        assert metrics.active_jobs == 50
        assert metrics.get_surge_tier() == "medium"

    def test_get_demand_metrics_cached(self):
        """Test retrieving cached demand metrics."""
        # Update metrics
        self.engine.update_demand_metrics(
            provider="gcp",
            region="us-central1",
            gpu_type="T4",
            capacity_utilization=0.60,
        )

        # Retrieve
        metrics = self.engine.get_demand_metrics("gcp", "us-central1", "T4")
        assert metrics is not None
        assert metrics.capacity_utilization == 0.60

    def test_get_demand_metrics_not_found(self):
        """Test retrieving non-existent demand metrics."""
        metrics = self.engine.get_demand_metrics("unknown", "unknown", None)
        assert metrics is None

    def test_calculate_dynamic_price_no_adjustments(self):
        """Test dynamic price with no adjustments applied."""
        result = self.engine.calculate_dynamic_price(
            base_price=1.00,
            provider="aws",
            region="us-east-1",
            gpu_type=None,
            apply_surge=False,
            apply_time_adjustment=False,
            apply_gpu_adjustment=False,
            apply_region_adjustment=False,
        )

        assert result["base_price"] == 1.00
        assert result["final_price"] == 1.00
        assert result["total_multiplier"] == 1.00

    def test_calculate_dynamic_price_with_time_adjustment(self):
        """Test dynamic price with time adjustment only."""
        result = self.engine.calculate_dynamic_price(
            base_price=1.00,
            provider="aws",
            region="us-east-1",
            apply_surge=False,
            apply_gpu_adjustment=False,
            apply_region_adjustment=False,
        )

        # Result depends on current time
        assert "time_of_day" in result["multipliers"]
        assert result["final_price"] >= 0.90  # Off-peak minimum
        assert result["final_price"] <= 1.15  # Peak maximum

    def test_calculate_dynamic_price_with_gpu_adjustment(self):
        """Test dynamic price with GPU adjustment."""
        result = self.engine.calculate_dynamic_price(
            base_price=1.00,
            provider="aws",
            region="us-east-1",
            gpu_type="H100",
            apply_surge=False,
            apply_time_adjustment=False,
            apply_region_adjustment=False,
        )

        # GPU demand multiplier is dynamic from catalog (1.00 when catalog empty)
        gpu_mult = result["multipliers"]["gpu_demand"]
        assert 0.90 <= gpu_mult <= 1.25

    def test_calculate_dynamic_price_with_surge(self):
        """Test dynamic price with surge pricing."""
        # First, set up high demand
        self.engine.update_demand_metrics(
            provider="aws",
            region="us-east-1",
            gpu_type="A100",
            capacity_utilization=0.92,  # High surge tier
        )

        result = self.engine.calculate_dynamic_price(
            base_price=1.00,
            provider="aws",
            region="us-east-1",
            gpu_type="A100",
            apply_surge=True,
            apply_time_adjustment=False,
            apply_gpu_adjustment=False,
            apply_region_adjustment=False,
        )

        assert result["surge_tier"] == "high"
        assert result["multipliers"]["surge"] == 1.50
        assert result["is_surge_active"] is True

    def test_get_price_forecast(self):
        """Test price forecasting."""
        forecasts = self.engine.get_price_forecast(
            base_price=1.00,
            provider="aws",
            region="us-east-1",
            gpu_type="A100",
            hours_ahead=24,
        )

        assert len(forecasts) == 24

        # Check that forecasts have expected fields
        for forecast in forecasts:
            assert "hours_from_now" in forecast
            assert "utc_hour" in forecast
            assert "estimated_price" in forecast
            assert "time_tier" in forecast
            assert "is_optimal" in forecast

    def test_find_optimal_time(self):
        """Test finding optimal run time."""
        result = self.engine.find_optimal_time(
            base_price=1.00,
            provider="aws",
            region="us-east-1",
            gpu_type="A100",
            within_hours=24,
        )

        assert "optimal_time" in result
        assert "current_price" in result
        assert "potential_savings" in result
        assert "wait_hours" in result


class TestStorageTracker:
    """Tests for the StorageTracker class."""

    def setup_method(self):
        """Set up test fixtures."""
        self.tracker = StorageTracker()

    def test_storage_price_constant(self):
        """Test that storage price is correctly defined."""
        assert self.tracker.STORAGE_PRICE_PER_GB_MONTH == Decimal("0.023")

    def test_record_usage(self):
        """Test recording storage usage."""
        usage = self.tracker.record_usage(
            user_id="user_123",
            checkpoint_bytes=1073741824,  # 1 GB
            artifact_bytes=536870912,     # 0.5 GB
            cache_bytes=268435456,        # 0.25 GB
        )

        assert usage.user_id == "user_123"
        assert usage.checkpoint_bytes == 1073741824
        assert usage.artifact_bytes == 536870912
        assert usage.cache_bytes == 268435456
        assert usage.total_bytes == 1073741824 + 536870912 + 268435456

        # Check cost calculation (1.75 GB * $0.023)
        expected_cost = Decimal("1.75") * Decimal("0.023")
        assert abs(float(usage.storage_cost) - float(expected_cost)) < 0.001

    def test_get_usage(self):
        """Test retrieving storage usage."""
        self.tracker.record_usage(
            user_id="user_456",
            checkpoint_bytes=1000000,
        )

        usage = self.tracker.get_usage("user_456")
        assert usage is not None
        assert usage.checkpoint_bytes == 1000000

    def test_get_usage_not_found(self):
        """Test retrieving non-existent usage."""
        usage = self.tracker.get_usage("nonexistent_user")
        assert usage is None

    def test_update_usage_incremental(self):
        """Test incremental usage updates."""
        # Initial usage
        self.tracker.record_usage(
            user_id="user_789",
            checkpoint_bytes=1000000,
            artifact_bytes=500000,
        )

        # Update with delta
        updated = self.tracker.update_usage(
            user_id="user_789",
            delta_checkpoint_bytes=500000,
            delta_artifact_bytes=-200000,  # Cleanup
        )

        assert updated.checkpoint_bytes == 1500000
        assert updated.artifact_bytes == 300000

    def test_update_usage_no_existing(self):
        """Test incremental update with no existing usage."""
        updated = self.tracker.update_usage(
            user_id="new_user",
            delta_checkpoint_bytes=1000000,
        )

        assert updated.checkpoint_bytes == 1000000
        assert updated.total_bytes == 1000000

    def test_calculate_storage_cost(self):
        """Test storage cost calculation."""
        self.tracker.record_usage(
            user_id="cost_user",
            checkpoint_bytes=5368709120,  # 5 GB
            artifact_bytes=2147483648,    # 2 GB
            cache_bytes=1073741824,       # 1 GB
        )

        cost_breakdown = self.tracker.calculate_storage_cost("cost_user")

        assert cost_breakdown["user_id"] == "cost_user"
        assert cost_breakdown["total_gb"] == 8.0

        # Check breakdown
        assert cost_breakdown["breakdown"]["checkpoints"]["gb"] == 5.0
        assert cost_breakdown["breakdown"]["artifacts"]["gb"] == 2.0
        assert cost_breakdown["breakdown"]["cache"]["gb"] == 1.0

        # Total cost should be 8 GB * $0.023 = $0.184
        expected_total = 8.0 * 0.023
        assert abs(cost_breakdown["monthly_cost"] - expected_total) < 0.001

    def test_calculate_storage_cost_no_usage(self):
        """Test storage cost for user with no usage."""
        cost_breakdown = self.tracker.calculate_storage_cost("no_usage_user")

        assert cost_breakdown["total_bytes"] == 0
        assert cost_breakdown["monthly_cost"] == 0.0

    def test_get_usage_summary(self):
        """Test getting usage summary across periods."""
        # Record usage for multiple periods
        self.tracker.record_usage(
            user_id="summary_user",
            checkpoint_bytes=1073741824,
            billing_period="2026-01",
        )
        self.tracker.record_usage(
            user_id="summary_user",
            checkpoint_bytes=2147483648,
            billing_period="2026-02",
        )

        summary = self.tracker.get_usage_summary("summary_user")

        assert summary["user_id"] == "summary_user"
        assert summary["total_bytes"] == 1073741824 + 2147483648
        assert len(summary["periods"]) == 2


class TestStorageUsage:
    """Tests for the StorageUsage dataclass."""

    def test_to_dict(self):
        """Test conversion to dictionary."""
        usage = StorageUsage(
            user_id="test_user",
            checkpoint_bytes=1000000,
            artifact_bytes=500000,
            cache_bytes=250000,
            total_bytes=1750000,
            storage_cost=Decimal("0.001"),
            billing_period="2026-01",
        )

        d = usage.to_dict()

        assert d["user_id"] == "test_user"
        assert d["checkpoint_bytes"] == 1000000
        assert d["storage_cost"] == 0.001
        assert "measured_at" in d


class TestDemandMetrics:
    """Tests for the DemandMetrics dataclass."""

    def test_get_surge_tier_boundary_none(self):
        """Test surge tier at 'none' boundary."""
        metrics = DemandMetrics(capacity_utilization=0.64)
        assert metrics.get_surge_tier() == "none"

        metrics = DemandMetrics(capacity_utilization=0.65)
        assert metrics.get_surge_tier() == "low"

    def test_get_surge_tier_boundary_extreme(self):
        """Test surge tier at 'extreme' boundary."""
        metrics = DemandMetrics(capacity_utilization=0.94)
        assert metrics.get_surge_tier() == "high"

        metrics = DemandMetrics(capacity_utilization=0.95)
        assert metrics.get_surge_tier() == "extreme"

        metrics = DemandMetrics(capacity_utilization=1.00)
        assert metrics.get_surge_tier() == "extreme"
