"""工具层测试：calc_engine 和 deviation_engine 核心函数"""
import pytest
from app.utils.deviation_engine import (
    calc_deviation,
    calc_period_deviation,
    calc_period_diff,
    check_trend_anomaly,
    run_deviation_check,
)
from app.utils.calc_engine import calculate_all, _get
from app.models import KPIDefinition, KPIValue, BudgetPlan, KPIAlert


class TestDeviationCalc:
    """偏差计算核心函数测试"""

    def test_calc_deviation_positive(self):
        """实际 > 预算 → 正差异"""
        result = calc_deviation(120, 100)
        assert result["deviation_amount"] == 20
        assert result["deviation_rate"] == 20.0
        assert result["is_over_budget"] is True

    def test_calc_deviation_negative(self):
        """实际 < 预算 → 负差异"""
        result = calc_deviation(80, 100)
        assert result["deviation_amount"] == -20
        assert result["deviation_rate"] == -20.0
        assert result["is_over_budget"] is False

    def test_calc_deviation_zero_budget(self):
        """预算为0 → 差异为None"""
        result = calc_deviation(100, 0)
        assert result["deviation_amount"] is None
        assert result["deviation_rate"] is None
        assert result["is_over_budget"] is None

    def test_calc_deviation_none_budget(self):
        """预算为None → 差异为None"""
        result = calc_deviation(100, None)
        assert result["deviation_amount"] is None

    def test_calc_deviation_exact_match(self):
        """实际==预算 → 差异0"""
        result = calc_deviation(100, 100)
        assert result["deviation_amount"] == 0
        assert result["deviation_rate"] == 0.0
        assert result["is_over_budget"] is False


class TestPeriodDeviation:
    """单期差异计算（依赖数据库）"""

    def test_calc_period_deviation_with_budget(self, db):
        """有预算值和实际值 → 正确计算差异"""
        kpi = KPIDefinition(kpi_code="TEST_DEV_001", kpi_name="测试KPI", dimension="finance",
                            target_value=1000, status="active")
        db.add(kpi)
        db.commit()

        bp = BudgetPlan(kpi_id=kpi.id, period="2026-06", budget_value=500.0,
                        budget_year=2026, budget_month=6, status="active", version="v1.0")
        db.add(bp)
        kv = KPIValue(kpi_id=kpi.id, period="2026-06", actual_value=600.0)
        db.add(kv)
        db.commit()

        result = calc_period_deviation(db, kpi.id, "2026-06")
        assert result["kpi_code"] == "TEST_DEV_001"
        assert result["actual_value"] == 600.0
        assert result["budget_value"] == 500.0
        assert result["deviation_amount"] == 100.0
        assert result["deviation_rate"] == 20.0

    def test_calc_period_deviation_no_budget_fallback_target(self, db):
        """无预算值时回退到 target_value/12（monthly频率）"""
        kpi = KPIDefinition(kpi_code="TEST_DEV_002", kpi_name="测试KPI", dimension="finance",
                            target_value=1200, frequency="monthly", status="active")
        db.add(kpi)
        db.flush()
        kv = KPIValue(kpi_id=kpi.id, period="2026-06", actual_value=150.0)
        db.add(kv)
        db.commit()

        result = calc_period_deviation(db, kpi.id, "2026-06")
        # target=1200, monthly budget = 1200/12 = 100
        assert result["budget_value"] == 100.0
        assert result["deviation_amount"] == 50.0


class TestPeriodDiff:
    """同比/环比差异测试"""

    def test_yoy_diff(self, db):
        """同比差异计算"""
        kpi = KPIDefinition(kpi_code="TEST_YOY", kpi_name="同比测试", dimension="finance", status="active")
        db.add(kpi)
        db.commit()
        # 上年同期值
        kv1 = KPIValue(kpi_id=kpi.id, period="2025-06", actual_value=100.0)
        # 本期值
        kv2 = KPIValue(kpi_id=kpi.id, period="2026-06", actual_value=120.0)
        db.add_all([kv1, kv2])
        db.commit()

        result = calc_period_diff(db, kpi.id, "2026-06", "yoy")
        assert result["label"] == "同比"
        assert result["current_value"] == 120.0
        assert result["prev_value"] == 100.0
        assert result["diff_amount"] == 20.0
        assert result["diff_rate"] == 20.0

    def test_mom_diff_january(self, db):
        """环比：跨年情况（1月→去年12月）"""
        kpi = KPIDefinition(kpi_code="TEST_MOM", kpi_name="环比测试", dimension="finance", status="active")
        db.add(kpi)
        db.commit()
        kv1 = KPIValue(kpi_id=kpi.id, period="2025-12", actual_value=200.0)
        kv2 = KPIValue(kpi_id=kpi.id, period="2026-01", actual_value=180.0)
        db.add_all([kv1, kv2])
        db.commit()

        result = calc_period_diff(db, kpi.id, "2026-01", "mom")
        assert result["label"] == "环比"
        assert result["prev_period"] == "2025-12"
        assert result["diff_amount"] == -20.0
        assert result["diff_rate"] == -10.0

    def test_diff_unknown_type(self, db):
        """未知比较类型"""
        kpi = KPIDefinition(kpi_code="TEST_UNKNOWN", kpi_name="未知", dimension="finance", status="active")
        db.add(kpi)
        db.commit()
        result = calc_period_diff(db, kpi.id, "2026-06", "wow")
        assert "error" in result


class TestTrendAnomaly:
    """趋势异常检测"""

    def test_continuous_decline(self, db):
        """连续3期下滑 → 触发异常"""
        kpi = KPIDefinition(kpi_code="TEST_TREND_DEC", kpi_name="下滑趋势",
                            dimension="finance", status="active")
        db.add(kpi)
        db.commit()
        for m, v in [(4, 150), (5, 120), (6, 90)]:
            db.add(KPIValue(kpi_id=kpi.id, period=f"2026-{m:02d}", actual_value=float(v)))
        db.commit()

        result = check_trend_anomaly(db, kpi.id, "2026-06", consecutive=3)
        assert result["anomaly"] is True
        assert result["type"] == "continuous_decline"
        assert "下滑" in result["message"]

    def test_continuous_rise(self, db):
        """连续3期上升 → 触发异常"""
        kpi = KPIDefinition(kpi_code="TEST_TREND_RISE", kpi_name="上升趋势",
                            dimension="finance", status="active")
        db.add(kpi)
        db.commit()
        for m, v in [(4, 50), (5, 80), (6, 110)]:
            db.add(KPIValue(kpi_id=kpi.id, period=f"2026-{m:02d}", actual_value=float(v)))
        db.commit()

        result = check_trend_anomaly(db, kpi.id, "2026-06", consecutive=3)
        assert result["anomaly"] is True
        assert result["type"] == "continuous_rise"

    def test_no_anomaly(self, db):
        """不满足连续条件 → 无异常"""
        kpi = KPIDefinition(kpi_code="TEST_TREND_NORMAL", kpi_name="正常趋势",
                            dimension="finance", status="active")
        db.add(kpi)
        db.commit()
        for m, v in [(4, 100), (5, 120), (6, 90)]:
            db.add(KPIValue(kpi_id=kpi.id, period=f"2026-{m:02d}", actual_value=float(v)))
        db.commit()

        result = check_trend_anomaly(db, kpi.id, "2026-06", consecutive=3)
        assert result["anomaly"] is False

    def test_insufficient_data(self, db):
        """数据不足 → 无异常"""
        kpi = KPIDefinition(kpi_code="TEST_TREND_SHORT", kpi_name="数据不足",
                            dimension="finance", status="active")
        db.add(kpi)
        db.flush()
        db.add(KPIValue(kpi_id=kpi.id, period="2026-06", actual_value=100.0))
        db.commit()

        result = check_trend_anomaly(db, kpi.id, "2026-06", consecutive=3)
        assert result["anomaly"] is False
        assert "数据不足" in result.get("reason", "")


class TestRunDeviationCheck:
    """run_deviation_check 集成测试"""

    def test_run_check_creates_alerts(self, db):
        """偏差超阈值 → 创建预警"""
        kpi = KPIDefinition(kpi_code="SALES_TOTAL", kpi_name="销售总额",
                            dimension="finance", target_value=100000, status="active")
        db.add(kpi)
        db.commit()

        # 预算50000 vs 实际40000 → 差异率-20%（higher_better，低于预算触发）
        bp = BudgetPlan(kpi_id=kpi.id, period="2026-06", budget_value=50000.0,
                        budget_year=2026, budget_month=6, status="active", version="v1.0")
        kv = KPIValue(kpi_id=kpi.id, period="2026-06", actual_value=40000.0)
        db.add_all([bp, kv])
        db.commit()

        count = run_deviation_check(db, period="2026-06")
        assert count >= 1

        alert = db.query(KPIAlert).filter(KPIAlert.kpi_id == kpi.id).first()
        assert alert is not None
        assert "差异预警" in alert.alert_message

    def test_run_check_no_alert_when_on_budget(self, db):
        """实际≈预算 → 不创建预警"""
        kpi = KPIDefinition(kpi_code="SALES_TOTAL", kpi_name="销售总额",
                            dimension="finance", target_value=100000, status="active")
        db.add(kpi)
        db.flush()
        bp = BudgetPlan(kpi_id=kpi.id, period="2026-06", budget_value=50000.0,
                        budget_year=2026, budget_month=6, status="active", version="v1.0")
        kv = KPIValue(kpi_id=kpi.id, period="2026-06", actual_value=50500.0)
        db.add_all([kpi, bp, kv])
        db.commit()

        count = run_deviation_check(db, period="2026-06")
        # 差异率1% < 10%阈值，不触发
        assert count == 0
