"""
Test variance and anomaly interaction to ensure anomalies remain visible.

Tests that anomaly patterns stand out clearly even when applied on top 
of variance processing using 90-day synthetic datasets.
"""

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

from src.generate.processing.anomalies import (
    apply_daily_spike,
    apply_daily_step_up,
    apply_hourly_spike
)
from src.generate.processing.variance import apply_variance


def create_test_dataframe_with_variance(
    start_date: datetime, 
    end_date: datetime, 
    base_cost: float = 100.0,
    daily_variance_pct: float = 50,
    hourly_variance_pct: float = 100
) -> pl.DataFrame:
    """
    Create synthetic dataset with variance applied (simulates generate pipeline).
    
    This mimics the actual pipeline order: base costs -> variance -> anomalies
    """
    # Generate hourly timestamps
    current = start_date
    timestamps = []
    while current <= end_date:
        timestamps.append(current)
        current += timedelta(hours=1)
    
    # Create base DataFrame
    base_df = pl.DataFrame({
        "timestamp": timestamps,
        "hourly_cost": [base_cost] * len(timestamps),
        "resource_id": ["test-resource-123"] * len(timestamps)
    })
    
    # Apply variance (simulates what happens in real pipeline)
    variance_config = {
        "daily_variance_pct": daily_variance_pct,
        "hourly_variance_pct": hourly_variance_pct
    }
    
    return apply_variance(base_df, variance_config, "test-resource-123")


@pytest.fixture
def test_90_day_varied_dataset():
    """90-day dataset with standard variance applied (50% daily, 100% hourly)"""
    return create_test_dataframe_with_variance(
        datetime(2024, 1, 1, 0, 0, 0),
        datetime(2024, 3, 31, 23, 0, 0),
        base_cost=100.0,
        daily_variance_pct=50,
        hourly_variance_pct=100
    )


@pytest.fixture
def test_90_day_high_variance_dataset():
    """90-day dataset with high variance (100% daily, 200% hourly)"""
    return create_test_dataframe_with_variance(
        datetime(2024, 1, 1, 0, 0, 0),
        datetime(2024, 3, 31, 23, 0, 0),
        base_cost=100.0,
        daily_variance_pct=100,
        hourly_variance_pct=200
    )


class TestVarianceAnomalyInteraction:
    """Test that anomalies remain clearly visible with variance applied."""
    
    def test_daily_spike_visibility_with_variance(self, test_90_day_varied_dataset):
        """Verify daily spikes are clearly visible above variance noise."""
        # Apply anomaly on top of varied data (simulates real pipeline)
        # Use auto-calculated intensity for guaranteed visibility with variance
        config = {
            "type": "daily_spike", 
            "period": "Last 30 Days",
            "variance_config": {"daily_variance_pct": 50, "hourly_variance_pct": 100}
        }
        result_df = apply_daily_spike(test_90_day_varied_dataset, config, "test-resource")
        
        # Get spike days and non-spike days
        spike_dates = [
            datetime(2024, 1, 16).date(),
            datetime(2024, 2, 15).date(), 
            datetime(2024, 3, 16).date()
        ]
        
        spike_costs = []
        normal_costs = []
        
        for date in spike_dates:
            day_data = result_df.filter(pl.col("timestamp").dt.date() == date)
            spike_costs.extend(day_data["hourly_cost"].to_list())
            
        # Sample normal days (not spike days)
        normal_dates = [
            datetime(2024, 1, 10).date(),
            datetime(2024, 2, 10).date(),
            datetime(2024, 3, 10).date()
        ]
        
        for date in normal_dates:
            day_data = result_df.filter(pl.col("timestamp").dt.date() == date)
            normal_costs.extend(day_data["hourly_cost"].to_list())
        
        # Statistical analysis: spike costs should be clearly higher
        avg_spike_cost = np.mean(spike_costs)
        avg_normal_cost = np.mean(normal_costs)
        max_normal_cost = np.max(normal_costs)
        min_spike_cost = np.min(spike_costs)
        
        # Anomalies should create clear separation
        separation_ratio = min_spike_cost / max_normal_cost
        
        print(f"Average spike cost: {avg_spike_cost:.2f}")
        print(f"Average normal cost: {avg_normal_cost:.2f}")
        print(f"Max normal cost: {max_normal_cost:.2f}")
        print(f"Min spike cost: {min_spike_cost:.2f}")
        print(f"Separation ratio: {separation_ratio:.2f}")
        
        # With 5x intensity, even minimum spike should be > maximum normal cost
        assert separation_ratio > 1.5, f"Insufficient separation: {separation_ratio}"
        assert avg_spike_cost > avg_normal_cost * 3, "Average spike not clearly higher"
        
    def test_step_up_visibility_with_high_variance(self, test_90_day_high_variance_dataset):
        """Verify step changes are visible even with high variance (200% hourly) using auto-calculation."""
        # Use auto-calculated intensity for guaranteed visibility with high variance
        config = {
            "type": "daily_step_up", 
            "period": "Last 30 Days",
            "variance_config": {"daily_variance_pct": 100, "hourly_variance_pct": 200}
        }
        result_df = apply_daily_step_up(test_90_day_high_variance_dataset, config, "test-resource")
        
        step_date = datetime(2024, 1, 16).date()
        
        # Get before/after periods
        before_data = result_df.filter(pl.col("timestamp").dt.date() < step_date)
        after_data = result_df.filter(pl.col("timestamp").dt.date() >= step_date)
        
        before_costs = before_data["hourly_cost"].to_list()
        after_costs = after_data["hourly_cost"].to_list()
        
        # Statistical analysis
        avg_before = np.mean(before_costs)
        avg_after = np.mean(after_costs)
        std_before = np.std(before_costs)
        std_after = np.std(after_costs)
        
        step_ratio = avg_after / avg_before
        
        print(f"Before step: avg={avg_before:.2f}, std={std_before:.2f}")
        print(f"After step: avg={avg_after:.2f}, std={std_after:.2f}")
        print(f"Step ratio: {step_ratio:.2f}")
        
        # With auto-calculated intensity, should see clear step change despite high variance
        assert step_ratio > 4.0, f"Step change not clearly visible with auto-calculation: {step_ratio}"
        
        # After costs should be consistently higher (virtually all values above before average)
        after_above_before_avg = sum(1 for cost in after_costs if cost > avg_before)
        after_above_ratio = after_above_before_avg / len(after_costs)
        
        assert after_above_ratio > 0.95, f"Only {after_above_ratio:.2%} of after-costs above before average with auto-calculation"
        
    def test_hourly_spike_pattern_detection_with_variance(self, test_90_day_varied_dataset):
        """Verify hourly spike patterns are detectable within varied data."""
        # Use auto-calculated intensity for guaranteed 4x separation
        config = {
            "type": "hourly_spike", 
            "period": "Last 30 Days",
            "variance_config": {"daily_variance_pct": 50, "hourly_variance_pct": 100}
        }
        result_df = apply_hourly_spike(test_90_day_varied_dataset, config, "test-resource")
        
        # Expected spike windows: 2 PM - 5 PM on spike days
        expected_spike_times = []
        spike_dates = [
            datetime(2024, 1, 16),
            datetime(2024, 2, 15),
            datetime(2024, 3, 16)
        ]
        
        for spike_date in spike_dates:
            for hour in [14, 15, 16, 17]:  # 2 PM - 5 PM
                expected_spike_times.append(spike_date.replace(hour=hour))
        
        # Get costs for spike windows vs adjacent hours
        spike_window_costs = []
        adjacent_costs = []  # Hours just before/after spikes
        
        for spike_time in expected_spike_times:
            # Spike hour cost
            spike_data = result_df.filter(pl.col("timestamp") == spike_time)
            if len(spike_data) > 0:
                spike_window_costs.append(spike_data["hourly_cost"][0])
            
            # Adjacent hour costs (hour before and after)
            before_hour = spike_time - timedelta(hours=1)
            after_hour = spike_time + timedelta(hours=1)
            
            before_data = result_df.filter(pl.col("timestamp") == before_hour)
            after_data = result_df.filter(pl.col("timestamp") == after_hour)
            
            if len(before_data) > 0:
                adjacent_costs.append(before_data["hourly_cost"][0])
            if len(after_data) > 0:
                adjacent_costs.append(after_data["hourly_cost"][0])
        
        # Pattern analysis
        avg_spike_cost = np.mean(spike_window_costs)
        avg_adjacent_cost = np.mean(adjacent_costs)
        
        pattern_ratio = avg_spike_cost / avg_adjacent_cost
        
        print(f"Spike window average: {avg_spike_cost:.2f}")
        print(f"Adjacent hours average: {avg_adjacent_cost:.2f}")
        print(f"Pattern detection ratio: {pattern_ratio:.2f}")
        
        # With variance applied to both spike and adjacent hours, the pattern ratio
        # will be lower than direct separation ratio. Adjust expectation to be realistic.
        # The key is that spikes are still clearly higher than adjacent hours.
        assert pattern_ratio > 1.2, f"Spike pattern not detectable: {pattern_ratio}"
        
        # Additional check: spike costs should be substantially higher than baseline
        baseline_cost = 100.0
        min_spike_cost = np.min(spike_window_costs)
        assert min_spike_cost > baseline_cost * 10, f"Spike costs {min_spike_cost} not substantially above baseline {baseline_cost}"
        assert len(spike_window_costs) == 12, f"Expected 12 spike windows, found {len(spike_window_costs)}"


class TestVarianceAnomalyMagnitudes:
    """Test appropriate intensity values for different variance levels."""
    
    def test_auto_vs_manual_intensity_comparison(self, test_90_day_varied_dataset):
        """Compare auto-calculated vs manual intensities for separation effectiveness."""
        variance_config = {"daily_variance_pct": 50, "hourly_variance_pct": 100}
        
        # Test auto-calculated intensity
        auto_config = {
            "type": "daily_spike", 
            "period": "Last 30 Days",
            "variance_config": variance_config
        }
        auto_result = apply_daily_spike(test_90_day_varied_dataset, auto_config, "test-resource")
        
        # Test old manual intensity (should be insufficient)
        manual_config = {"type": "daily_spike", "period": "Last 30 Days", "intensity": 3.0}
        manual_result = apply_daily_spike(test_90_day_varied_dataset, manual_config, "test-resource")
        
        # Calculate separation ratios
        spike_date = datetime(2024, 1, 16).date()
        normal_date = datetime(2024, 1, 10).date()
        
        # Auto-calculated results
        auto_spike_costs = auto_result.filter(pl.col("timestamp").dt.date() == spike_date)["hourly_cost"].to_list()
        auto_normal_costs = auto_result.filter(pl.col("timestamp").dt.date() == normal_date)["hourly_cost"].to_list()
        auto_separation = np.min(auto_spike_costs) / np.max(auto_normal_costs)
        
        # Manual intensity results  
        manual_spike_costs = manual_result.filter(pl.col("timestamp").dt.date() == spike_date)["hourly_cost"].to_list()
        manual_normal_costs = manual_result.filter(pl.col("timestamp").dt.date() == normal_date)["hourly_cost"].to_list()
        manual_separation = np.min(manual_spike_costs) / np.max(manual_normal_costs)
        
        print(f"Auto-calculated separation: {auto_separation:.2f}")
        print(f"Manual 3x separation: {manual_separation:.2f}")
        
        # Auto-calculation should achieve 4x+ separation target
        assert auto_separation > 4.0, f"Auto-calculated separation {auto_separation} below 4x target"
        
        # Manual should be insufficient (demonstrating the problem we solved)
        assert manual_separation < 2.0, f"Manual intensity unexpectedly sufficient: {manual_separation}"
        
        # Auto should be significantly better than manual
        assert auto_separation > manual_separation * 2, "Auto-calculation should be much better than manual"


class TestVarianceAnomalyCombinedPatterns:
    """Test complex scenarios with both variance and anomalies."""
    
    def test_multiple_anomaly_types_with_variance(self, test_90_day_varied_dataset):
        """Test dataset with both spikes and step changes applied."""
        variance_config = {"daily_variance_pct": 50, "hourly_variance_pct": 100}
        
        # Apply step up first (permanent change from Jan 16) using auto-calculation
        step_config = {
            "type": "daily_step_up", 
            "period": "Last 30 Days",
            "variance_config": variance_config
        }
        step_result = apply_daily_step_up(test_90_day_varied_dataset, step_config, "test-resource")
        
        # Then apply spikes on top (should spike the already-stepped costs) using auto-calculation
        spike_config = {
            "type": "daily_spike", 
            "period": "Last 30 Days",
            "variance_config": variance_config
        }
        final_result = apply_daily_spike(step_result, spike_config, "test-resource")
        
        # Analyze the compound effects
        step_date = datetime(2024, 1, 16).date()
        spike_date = datetime(2024, 1, 16).date()  # Same day for complex interaction
        
        # Jan 16 should have BOTH auto-calculated step up AND auto-calculated spike = multiplicative effect
        combined_day = final_result.filter(pl.col("timestamp").dt.date() == spike_date)
        normal_day = final_result.filter(pl.col("timestamp").dt.date() == datetime(2024, 1, 10).date())
        
        combined_avg = np.mean(combined_day["hourly_cost"].to_list())
        normal_avg = np.mean(normal_day["hourly_cost"].to_list())
        
        compound_ratio = combined_avg / normal_avg
        
        print(f"Combined effects ratio: {compound_ratio:.2f}")
        print(f"Expected large multiplicative effect from auto-calculated intensities")
        
        # With auto-calculated intensities (16x step * 20x spike), should see massive compound effect
        assert compound_ratio > 100.0, f"Auto-calculated compound effects not visible: {compound_ratio}"