"""
Tests for Safety Stock Optimizer
"""

import pytest
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from app.ml.safety_stock_optimizer import SafetyStockOptimizer


@pytest.fixture
def stable_demand_history():
    """Generate demand history with stable, predictable demand."""
    dates = pd.date_range(start='2024-01-01', end='2024-12-31', freq='D')

    # Stable demand: mean=100, low variance
    demands = np.random.normal(100, 10, len(dates))
    demands = np.maximum(demands, 0)  # No negative demand

    data = {
        'date': dates,
        'demand_quantity': demands,
        'lead_time_days': [3] * len(dates),
        'stockout': [False] * len(dates)
    }

    return pd.DataFrame(data)


@pytest.fixture
def variable_demand_history():
    """Generate demand history with high variability."""
    dates = pd.date_range(start='2024-01-01', end='2024-12-31', freq='D')

    # Variable demand: mean=100, high variance
    demands = np.random.normal(100, 40, len(dates))
    demands = np.maximum(demands, 0)

    # Add some stockouts (10%)
    stockouts = np.random.random(len(dates)) < 0.1

    data = {
        'date': dates,
        'demand_quantity': demands,
        'lead_time_days': np.random.normal(3, 1, len(dates)),
        'stockout': stockouts
    }

    return pd.DataFrame(data)


@pytest.fixture
def high_criticality_product():
    """Product characteristics for high criticality product."""
    return {
        'shelf_life_days': 7,
        'criticality': 'high',
        'unit_cost': 15.0,
        'avg_daily_demand': 100
    }


@pytest.fixture
def low_criticality_product():
    """Product characteristics for low criticality product."""
    return {
        'shelf_life_days': 30,
        'criticality': 'low',
        'unit_cost': 2.0,
        'avg_daily_demand': 50
    }


@pytest.fixture
def cost_parameters():
    """Standard cost parameters."""
    return {
        'holding_cost_per_unit_per_day': 0.01,
        'stockout_cost_per_unit': 10.0
    }


@pytest.mark.asyncio
async def test_optimize_stable_demand(stable_demand_history, high_criticality_product):
    """Test optimization with stable demand."""
    optimizer = SafetyStockOptimizer()

    results = await optimizer.optimize_safety_stock(
        tenant_id='test-tenant',
        inventory_product_id='stable-product',
        demand_history=stable_demand_history,
        product_characteristics=high_criticality_product,
        min_history_days=90
    )

    # Check structure
    assert 'tenant_id' in results
    assert 'inventory_product_id' in results
    assert 'optimal_result' in results
    assert 'hardcoded_result' in results
    assert 'comparison' in results
    assert 'insights' in results

    # Stable demand should have lower safety stock
    optimal = results['optimal_result']
    assert optimal['safety_stock'] > 0
    assert 0.90 <= optimal['service_level'] <= 0.99


@pytest.mark.asyncio
async def test_optimize_variable_demand(variable_demand_history, high_criticality_product):
    """Test optimization with variable demand."""
    optimizer = SafetyStockOptimizer()

    results = await optimizer.optimize_safety_stock(
        tenant_id='test-tenant',
        inventory_product_id='variable-product',
        demand_history=variable_demand_history,
        product_characteristics=high_criticality_product,
        min_history_days=90
    )

    optimal = results['optimal_result']

    # Variable demand should require higher safety stock
    assert optimal['safety_stock'] > 0

    # Should achieve high service level for high criticality
    assert optimal['service_level'] >= 0.95


@pytest.mark.asyncio
async def test_demand_statistics_calculation(stable_demand_history):
    """Test demand statistics calculation."""
    optimizer = SafetyStockOptimizer()

    results = await optimizer.optimize_safety_stock(
        tenant_id='test-tenant',
        inventory_product_id='test-product',
        demand_history=stable_demand_history,
        product_characteristics={'criticality': 'medium', 'avg_daily_demand': 100}
    )

    stats = results['demand_stats']

    # Check all required statistics present
    required_stats = [
        'mean_demand', 'std_demand', 'cv_demand',
        'min_demand', 'max_demand',
        'mean_lead_time', 'std_lead_time',
        'stockout_rate', 'data_points'
    ]

    for stat in required_stats:
        assert stat in stats, f"Missing statistic: {stat}"

    # Check values are reasonable
    assert stats['mean_demand'] > 0
    assert stats['std_demand'] >= 0
    assert 0 <= stats['cv_demand'] <= 2
    assert stats['mean_lead_time'] > 0
    assert 0 <= stats['stockout_rate'] <= 1
    assert stats['data_points'] == len(stable_demand_history)


@pytest.mark.asyncio
async def test_statistical_safety_stock_calculation():
    """Test statistical safety stock calculation method."""
    optimizer = SafetyStockOptimizer()

    demand_stats = {
        'mean_demand': 100,
        'std_demand': 20,
        'mean_lead_time': 3,
        'std_lead_time': 0.5,
        'cv_demand': 0.2
    }

    product_chars = {
        'criticality': 'high',
        'avg_daily_demand': 100
    }

    result = optimizer._calculate_statistical_safety_stock(
        demand_stats, product_chars, supplier_reliability=0.95
    )

    # Check structure
    assert 'method' in result
    assert result['method'] == 'statistical'
    assert 'safety_stock' in result
    assert 'service_level' in result
    assert 'z_score' in result

    # High criticality should get 98% service level
    assert result['service_level'] == 0.98
    assert result['safety_stock'] > 0


@pytest.mark.asyncio
async def test_criticality_affects_service_level():
    """Test that product criticality affects target service level."""
    optimizer = SafetyStockOptimizer()

    demand_stats = {
        'mean_demand': 100,
        'std_demand': 20,
        'mean_lead_time': 3,
        'std_lead_time': 0.5
    }

    # High criticality
    high_result = optimizer._calculate_statistical_safety_stock(
        demand_stats,
        {'criticality': 'high', 'avg_daily_demand': 100},
        None
    )

    # Low criticality
    low_result = optimizer._calculate_statistical_safety_stock(
        demand_stats,
        {'criticality': 'low', 'avg_daily_demand': 100},
        None
    )

    # High criticality should have higher service level and safety stock
    assert high_result['service_level'] > low_result['service_level']
    assert high_result['safety_stock'] > low_result['safety_stock']


@pytest.mark.asyncio
async def test_cost_based_optimization(stable_demand_history, high_criticality_product, cost_parameters):
    """Test cost-based safety stock optimization."""
    optimizer = SafetyStockOptimizer()

    results = await optimizer.optimize_safety_stock(
        tenant_id='test-tenant',
        inventory_product_id='test-product',
        demand_history=stable_demand_history,
        product_characteristics=high_criticality_product,
        cost_parameters=cost_parameters
    )

    optimal = results['optimal_result']

    # Should use cost optimization method for high-value product
    # (unit_cost > 5)
    assert optimal['method'] == 'cost_optimization'
    assert 'annual_total_cost' in optimal
    assert optimal['annual_total_cost'] > 0


@pytest.mark.asyncio
async def test_comparison_with_hardcoded(stable_demand_history, high_criticality_product, cost_parameters):
    """Test comparison between optimal and hardcoded approaches."""
    optimizer = SafetyStockOptimizer()

    results = await optimizer.optimize_safety_stock(
        tenant_id='test-tenant',
        inventory_product_id='test-product',
        demand_history=stable_demand_history,
        product_characteristics=high_criticality_product,
        cost_parameters=cost_parameters
    )

    comparison = results['comparison']

    # Check comparison metrics present
    assert 'stock_difference' in comparison
    assert 'stock_difference_pct' in comparison
    assert 'optimal_service_level' in comparison
    assert 'hardcoded_service_level' in comparison

    # Should have cost savings calculation
    if cost_parameters:
        assert 'annual_holding_cost_savings' in comparison


@pytest.mark.asyncio
async def test_over_stocking_insight_generation(stable_demand_history, low_criticality_product, cost_parameters):
    """Test insight generation when product is over-stocked."""
    optimizer = SafetyStockOptimizer()

    # Low criticality product with stable demand should recommend reduction
    results = await optimizer.optimize_safety_stock(
        tenant_id='test-tenant',
        inventory_product_id='overstocked-product',
        demand_history=stable_demand_history,
        product_characteristics=low_criticality_product,
        cost_parameters=cost_parameters
    )

    insights = results['insights']

    # Should generate insights
    assert len(insights) > 0

    # Check for reduction insight (if optimal is significantly lower)
    comparison = results['comparison']
    if comparison['stock_difference_pct'] < -10:
        reduction_insights = [i for i in insights if 'reduce' in i.get('title', '').lower()]
        assert len(reduction_insights) > 0

        insight = reduction_insights[0]
        assert insight['type'] == 'optimization'
        assert insight['impact_type'] == 'cost_savings'
        assert 'actionable' in insight
        assert insight['actionable'] is True


@pytest.mark.asyncio
async def test_under_stocking_insight_generation(variable_demand_history, high_criticality_product):
    """Test insight generation when product is under-stocked."""
    optimizer = SafetyStockOptimizer()

    # High criticality with variable demand should need more stock
    results = await optimizer.optimize_safety_stock(
        tenant_id='test-tenant',
        inventory_product_id='understocked-product',
        demand_history=variable_demand_history,
        product_characteristics=high_criticality_product
    )

    insights = results['insights']

    # Check for increase recommendation
    comparison = results['comparison']
    if comparison['stock_difference_pct'] > 10:
        increase_insights = [i for i in insights if 'increase' in i.get('title', '').lower()]

        if increase_insights:
            insight = increase_insights[0]
            assert insight['type'] in ['alert', 'recommendation']
            assert 'recommendation_actions' in insight


@pytest.mark.asyncio
async def test_high_variability_insight(variable_demand_history, high_criticality_product):
    """Test insight generation for high demand variability."""
    optimizer = SafetyStockOptimizer()

    results = await optimizer.optimize_safety_stock(
        tenant_id='test-tenant',
        inventory_product_id='variable-product',
        demand_history=variable_demand_history,
        product_characteristics=high_criticality_product
    )

    insights = results['insights']
    stats = results['demand_stats']

    # If CV > 0.5, should generate variability insight
    if stats['cv_demand'] > 0.5:
        variability_insights = [i for i in insights if 'variability' in i.get('title', '').lower()]
        assert len(variability_insights) > 0


@pytest.mark.asyncio
async def test_stockout_alert_generation():
    """Test alert generation for frequent stockouts."""
    # Create demand history with frequent stockouts
    dates = pd.date_range(start='2024-01-01', end='2024-12-31', freq='D')

    data = {
        'date': dates,
        'demand_quantity': np.random.normal(100, 20, len(dates)),
        'lead_time_days': [3] * len(dates),
        'stockout': np.random.random(len(dates)) < 0.15  # 15% stockout rate
    }

    demand_history = pd.DataFrame(data)

    optimizer = SafetyStockOptimizer()

    results = await optimizer.optimize_safety_stock(
        tenant_id='test-tenant',
        inventory_product_id='stockout-product',
        demand_history=demand_history,
        product_characteristics={'criticality': 'high', 'avg_daily_demand': 100}
    )

    insights = results['insights']

    # Should generate stockout alert
    stockout_insights = [i for i in insights if 'stockout' in i.get('title', '').lower()]
    assert len(stockout_insights) > 0

    insight = stockout_insights[0]
    assert insight['priority'] in ['high', 'critical']
    assert insight['type'] == 'alert'


@pytest.mark.asyncio
async def test_shelf_life_constraint():
    """Test that shelf life constrains safety stock."""
    dates = pd.date_range(start='2024-01-01', end='2024-12-31', freq='D')

    demand_history = pd.DataFrame({
        'date': dates,
        'demand_quantity': np.random.normal(100, 30, len(dates)),  # High variance
        'lead_time_days': [3] * len(dates),
        'stockout': [False] * len(dates)
    })

    # Product with short shelf life
    product_chars = {
        'shelf_life_days': 3,  # Very short shelf life
        'criticality': 'high',
        'unit_cost': 5.0,
        'avg_daily_demand': 100
    }

    optimizer = SafetyStockOptimizer()

    results = await optimizer.optimize_safety_stock(
        tenant_id='test-tenant',
        inventory_product_id='perishable-product',
        demand_history=demand_history,
        product_characteristics=product_chars
    )

    optimal = results['optimal_result']

    # Safety stock should be constrained by shelf life
    # Max allowed = avg_daily_demand * (shelf_life * 0.5)
    max_allowed = product_chars['avg_daily_demand'] * (product_chars['shelf_life_days'] * 0.5)

    assert optimal['safety_stock'] <= max_allowed + 1  # Allow small rounding

    # Should have constraint flag
    if optimal['safety_stock'] >= max_allowed - 1:
        assert optimal.get('constrained_by') == 'shelf_life'


@pytest.mark.asyncio
async def test_supplier_reliability_adjustment():
    """Test that low supplier reliability increases safety stock."""
    dates = pd.date_range(start='2024-01-01', end='2024-12-31', freq='D')

    demand_history = pd.DataFrame({
        'date': dates,
        'demand_quantity': np.random.normal(100, 15, len(dates)),
        'lead_time_days': [3] * len(dates),
        'stockout': [False] * len(dates)
    })

    product_chars = {
        'criticality': 'medium',
        'avg_daily_demand': 100
    }

    optimizer = SafetyStockOptimizer()

    # Good supplier
    results_good = await optimizer.optimize_safety_stock(
        tenant_id='test-tenant',
        inventory_product_id='test-product',
        demand_history=demand_history,
        product_characteristics=product_chars,
        supplier_reliability=0.98
    )

    # Poor supplier
    results_poor = await optimizer.optimize_safety_stock(
        tenant_id='test-tenant',
        inventory_product_id='test-product',
        demand_history=demand_history,
        product_characteristics=product_chars,
        supplier_reliability=0.85
    )

    # Poor supplier should require higher safety stock
    assert results_poor['optimal_result']['safety_stock'] >= results_good['optimal_result']['safety_stock']


@pytest.mark.asyncio
async def test_insufficient_data_handling():
    """Test handling of insufficient demand history."""
    # Only 30 days (less than min_history_days=90)
    dates = pd.date_range(start='2024-01-01', end='2024-01-30', freq='D')

    small_history = pd.DataFrame({
        'date': dates,
        'demand_quantity': np.random.normal(100, 15, len(dates)),
        'lead_time_days': [3] * len(dates),
        'stockout': [False] * len(dates)
    })

    product_chars = {
        'criticality': 'high',
        'avg_daily_demand': 100,
        'shelf_life_days': 7
    }

    optimizer = SafetyStockOptimizer()

    results = await optimizer.optimize_safety_stock(
        tenant_id='test-tenant',
        inventory_product_id='new-product',
        demand_history=small_history,
        product_characteristics=product_chars,
        min_history_days=90
    )

    # Should return fallback response
    assert results['history_days'] == 0
    assert results['optimal_result']['method'] == 'fallback_heuristic'

    # Should use simple heuristic (7 days for high criticality)
    expected_safety_stock = product_chars['avg_daily_demand'] * 7
    assert results['optimal_result']['safety_stock'] == expected_safety_stock


def test_get_optimal_safety_stock():
    """Test retrieval of cached optimal safety stock."""
    optimizer = SafetyStockOptimizer()

    # Initially no cached value
    assert optimizer.get_optimal_safety_stock('product-1') is None

    # Set a value
    optimizer.optimal_stocks['product-1'] = 150.5

    # Should retrieve it
    assert optimizer.get_optimal_safety_stock('product-1') == 150.5


def test_get_learned_service_level():
    """Test retrieval of learned service levels."""
    optimizer = SafetyStockOptimizer()

    # Initially no learned level
    assert optimizer.get_learned_service_level('product-1') is None

    # Set a level
    optimizer.learned_service_levels['product-1'] = 0.96

    # Should retrieve it
    assert optimizer.get_learned_service_level('product-1') == 0.96


@pytest.mark.asyncio
async def test_hardcoded_comparison():
    """Test comparison specifically highlights hardcoded vs optimal."""
    dates = pd.date_range(start='2024-01-01', end='2024-12-31', freq='D')

    demand_history = pd.DataFrame({
        'date': dates,
        'demand_quantity': np.random.normal(100, 15, len(dates)),
        'lead_time_days': [3] * len(dates),
        'stockout': [False] * len(dates)
    })

    optimizer = SafetyStockOptimizer()

    results = await optimizer.optimize_safety_stock(
        tenant_id='test-tenant',
        inventory_product_id='test-product',
        demand_history=demand_history,
        product_characteristics={'criticality': 'medium', 'avg_daily_demand': 100}
    )

    # Hardcoded should always be 95% service level
    assert results['hardcoded_result']['service_level'] == 0.95
    assert results['hardcoded_result']['method'] == 'hardcoded_95_service_level'

    # Comparison should show difference
    comparison = results['comparison']
    assert 'stock_difference' in comparison
    assert 'service_level_difference' in comparison


@pytest.mark.asyncio
async def test_multiple_products_caching():
    """Test that optimizer caches results for multiple products."""
    optimizer = SafetyStockOptimizer()

    dates = pd.date_range(start='2024-01-01', end='2024-12-31', freq='D')

    # Optimize multiple products
    for i in range(3):
        demand_history = pd.DataFrame({
            'date': dates,
            'demand_quantity': np.random.normal(100 + i*10, 15, len(dates)),
            'lead_time_days': [3] * len(dates),
            'stockout': [False] * len(dates)
        })

        await optimizer.optimize_safety_stock(
            tenant_id='test-tenant',
            inventory_product_id=f'product-{i}',
            demand_history=demand_history,
            product_characteristics={'criticality': 'medium', 'avg_daily_demand': 100 + i*10}
        )

    # Should have cached all three
    assert len(optimizer.optimal_stocks) == 3
    assert len(optimizer.learned_service_levels) == 3

    # Each should be retrievable
    for i in range(3):
        assert optimizer.get_optimal_safety_stock(f'product-{i}') is not None
        assert optimizer.get_learned_service_level(f'product-{i}') is not None
