"""
Comprehensive unit tests for anomaly profile processing.

Tests anomaly timing, intensity application, recurring patterns,
and edge cases using 90-day synthetic datasets.
"""

import pytest
from datetime import datetime, timedelta
import polars as pl

from src.generate.processing.anomalies import (
    apply_daily_spike,
    apply_daily_step_up, 
    apply_daily_step_down,
    apply_hourly_spike,
    apply_hourly_step_up,
    apply_hourly_step_down
)


def create_test_dataframe(start_date: datetime, end_date: datetime, base_cost: float = 100.0) -> pl.DataFrame:
    """
    Create synthetic hourly cost data for testing.
    
    Args:
        start_date: Start of test period
        end_date: End of test period  
        base_cost: Base hourly cost for flat baseline
        
    Returns:
        DataFrame with hourly timestamps and flat cost baseline
    """
    # Generate hourly timestamps
    current = start_date
    timestamps = []
    while current <= end_date:
        timestamps.append(current)
        current += timedelta(hours=1)
    
    return pl.DataFrame({
        "timestamp": timestamps,
        "hourly_cost": [base_cost] * len(timestamps),
        "resource_id": ["test-resource-123"] * len(timestamps)
    })


@pytest.fixture
def test_90_day_dataset():
    """90-day test dataset: Jan 1 - Mar 31, 2024 (~2160 hours)"""
    return create_test_dataframe(
        datetime(2024, 1, 1, 0, 0, 0),  # Start at midnight
        datetime(2024, 3, 31, 23, 0, 0),  # End at 11 PM on last day
        base_cost=100.0
    )


class TestTimingValidation:
    """Test that anomalies occur at correct calculated times."""
    
    def test_daily_spike_timing(self, test_90_day_dataset):
        """Verify daily spikes occur on Day 15 of each 30-day period (3-day duration each)."""
        config = {"type": "daily_spike", "period": "Last 30 Days", "intensity": 5.0}
        result_df = apply_daily_spike(test_90_day_dataset, config, "test-resource")
        
        # Expected 3-day spike periods: Day 15-17 of each 30-day period
        expected_spike_dates = [
            # First spike (Jan 16-18)
            datetime(2024, 1, 16).date(),
            datetime(2024, 1, 17).date(),
            datetime(2024, 1, 18).date(),
            # Second spike (Feb 15-17)
            datetime(2024, 2, 15).date(),
            datetime(2024, 2, 16).date(),
            datetime(2024, 2, 17).date(),
            # Third spike (Mar 16-18)
            datetime(2024, 3, 16).date(),
            datetime(2024, 3, 17).date(),
            datetime(2024, 3, 18).date(),
        ]
        
        # Find all days with spiked costs
        spiked_days = result_df.filter(pl.col("hourly_cost") == 500.0).select(
            pl.col("timestamp").dt.date()
        ).unique().sort("timestamp")
        
        actual_spike_dates = spiked_days["timestamp"].to_list()
        
        assert len(actual_spike_dates) == 9, f"Expected 9 spike days (3 periods × 3 days each), got {len(actual_spike_dates)}"
        assert actual_spike_dates == expected_spike_dates, f"Expected {expected_spike_dates}, got {actual_spike_dates}"
        
    def test_hourly_spike_timing(self, test_90_day_dataset):
        """Verify hourly spikes occur at correct times (2 PM - 6 PM on spike days)."""
        config = {"type": "hourly_spike", "period": "Last 30 Days", "intensity": 8.0}
        result_df = apply_hourly_spike(test_90_day_dataset, config, "test-resource")
        
        # Check first expected spike: Jan 16, 2024, 2 PM - 6 PM
        jan_16_data = result_df.filter(pl.col("timestamp").dt.date() == datetime(2024, 1, 16).date())
        
        # Hours 14-17 (2 PM - 5 PM inclusive) should be spiked (4-hour duration)
        spiked_hours = jan_16_data.filter(pl.col("hourly_cost") == 800.0)
        spike_hours = spiked_hours.select(pl.col("timestamp").dt.hour()).unique().sort("timestamp")
        expected_hours = [14, 15, 16, 17]
        
        actual_hours = spike_hours["timestamp"].to_list()
        assert actual_hours == expected_hours, f"Expected spike hours {expected_hours}, got {actual_hours}"
        
        # Other hours on same day should be normal
        normal_hours = jan_16_data.filter(pl.col("hourly_cost") == 100.0)
        assert len(normal_hours) == 20, f"Expected 20 normal hours on spike day, got {len(normal_hours)}"


class TestIntensityApplication:
    """Test that intensity multipliers are applied correctly."""
    
    def test_step_up_intensity(self, test_90_day_dataset):
        """Verify step up creates permanent cost increase from first occurrence."""
        config = {"type": "daily_step_up", "period": "Last 30 Days", "intensity": 3.0}
        result_df = apply_daily_step_up(test_90_day_dataset, config, "test-resource")
        
        first_step_date = datetime(2024, 1, 16).date()
        
        # Before step: original cost
        before_step = result_df.filter(pl.col("timestamp").dt.date() < first_step_date)
        before_costs = before_step["hourly_cost"].unique()
        assert before_costs.to_list() == [100.0], f"Expected [100.0] before step, got {before_costs.to_list()}"
        
        # After step: multiplied cost permanently
        after_step = result_df.filter(pl.col("timestamp").dt.date() >= first_step_date)
        after_costs = after_step["hourly_cost"].unique()
        assert after_costs.to_list() == [300.0], f"Expected [300.0] after step, got {after_costs.to_list()}"
        
    def test_step_down_intensity(self, test_90_day_dataset):
        """Verify step down creates permanent cost decrease."""
        config = {"type": "daily_step_down", "period": "Last 30 Days", "intensity": 0.4}
        result_df = apply_daily_step_down(test_90_day_dataset, config, "test-resource")
        
        first_step_date = datetime(2024, 1, 16).date()
        
        # After step down: reduced cost permanently  
        after_step = result_df.filter(pl.col("timestamp").dt.date() >= first_step_date)
        after_costs = after_step["hourly_cost"].unique()
        assert after_costs.to_list() == [40.0], f"Expected [40.0] after step down, got {after_costs.to_list()}"


class TestRecurringPatterns:
    """Test that anomalies repeat correctly across multiple periods."""
    
    def test_recurring_daily_spikes(self, test_90_day_dataset):
        """Verify daily spikes occur in each 30-day period (3-day duration each)."""
        config = {"type": "daily_spike", "period": "Last 30 Days", "intensity": 4.0}
        result_df = apply_daily_spike(test_90_day_dataset, config, "test-resource")
        
        # Count total spike days (should be 9 = 3 periods × 3 days each)
        spike_days = result_df.filter(pl.col("hourly_cost") == 400.0).select(
            pl.col("timestamp").dt.date()
        ).unique()
        
        assert len(spike_days) == 9, f"Expected 9 recurring spike days (3 periods × 3 days each), got {len(spike_days)}"
        
    def test_recurring_hourly_spikes(self, test_90_day_dataset):
        """Verify hourly spikes occur in each 30-day period.""" 
        config = {"type": "hourly_spike", "period": "Last 30 Days", "intensity": 6.0}
        result_df = apply_hourly_spike(test_90_day_dataset, config, "test-resource")
        
        # Count total spiked hours (should be 3 periods × 4 hours = 12 hours)
        spiked_hours = result_df.filter(pl.col("hourly_cost") == 600.0)
        
        assert len(spiked_hours) == 12, f"Expected 12 spiked hours (3 periods × 4 hours), got {len(spiked_hours)}"


class TestEdgeCases:
    """Test edge cases and boundary conditions."""
    
    def test_step_change_permanence(self, test_90_day_dataset):
        """Verify step changes remain permanent through end of dataset."""
        config = {"type": "daily_step_down", "period": "Last 30 Days", "intensity": 0.5}
        result_df = apply_daily_step_down(test_90_day_dataset, config, "test-resource")
        
        # After first step down, ALL subsequent data should be reduced
        first_step = datetime(2024, 1, 16).date()
        end_date = datetime(2024, 3, 31).date()
        
        after_step = result_df.filter(
            (pl.col("timestamp").dt.date() >= first_step) &
            (pl.col("timestamp").dt.date() <= end_date)
        )
        
        # Every single hour after step should be 50.0 (100 * 0.5)
        after_costs = after_step["hourly_cost"].unique()
        assert after_costs.to_list() == [50.0], f"Expected permanent [50.0] after step down, got {after_costs.to_list()}"
        
    def test_empty_dataframe_handling(self):
        """Verify anomaly functions handle empty DataFrames gracefully."""
        empty_df = pl.DataFrame({
            "timestamp": [],
            "hourly_cost": [],
            "resource_id": []
        }).with_columns([
            pl.col("timestamp").cast(pl.Datetime),
            pl.col("hourly_cost").cast(pl.Float64),
            pl.col("resource_id").cast(pl.Utf8)
        ])
        
        config = {"type": "daily_spike", "period": "Last 30 Days", "intensity": 5.0}
        result_df = apply_daily_spike(empty_df, config, "test-resource")
        
        assert len(result_df) == 0, "Expected empty DataFrame to remain empty"
        assert result_df.columns == empty_df.columns, "Expected columns to be preserved"


class TestConfigurationValidation:
    """Test configuration validation and error handling."""
    
    def test_invalid_period(self, test_90_day_dataset):
        """Verify invalid periods are rejected."""
        invalid_config = {"type": "daily_spike", "period": "Last 7 Days", "intensity": 4.0}
        
        with pytest.raises(ValueError, match="Only 'Last 30 Days' is supported"):
            apply_daily_spike(test_90_day_dataset, invalid_config, "test-resource")
            
    def test_invalid_anomaly_type(self, test_90_day_dataset):
        """Verify invalid anomaly types are rejected."""
        invalid_config = {"type": "invalid_type", "period": "Last 30 Days", "intensity": 4.0}
        
        with pytest.raises(ValueError, match="Unsupported anomaly type"):
            apply_daily_spike(test_90_day_dataset, invalid_config, "test-resource")
            
    def test_deprecated_parameters(self, test_90_day_dataset):
        """Verify deprecated day/hour parameters are rejected."""
        invalid_configs = [
            {"type": "daily_spike", "period": "Last 30 Days", "day": 7, "intensity": 4.0},
            {"type": "hourly_spike", "period": "Last 30 Days", "hour": 14, "intensity": 4.0},
            {"type": "hourly_spike", "period": "Last 30 Days", "duration_hours": 6, "intensity": 4.0},
        ]
        
        for config in invalid_configs:
            with pytest.raises(ValueError, match="Deprecated parameters"):
                apply_daily_spike(test_90_day_dataset, config, "test-resource")
                
    def test_missing_required_parameters(self, test_90_day_dataset):
        """Verify missing required parameters are rejected."""
        incomplete_configs = [
            {"period": "Last 30 Days", "intensity": 4.0},  # Missing type
            {"type": "daily_spike", "intensity": 4.0},     # Missing period
            {"type": "daily_spike", "period": "Last 30 Days"},  # Missing intensity
        ]
        
        for config in incomplete_configs:
            with pytest.raises(ValueError):
                apply_daily_spike(test_90_day_dataset, config, "test-resource")