from datetime import datetime, date, timezone
from time import perf_counter

import pytest

from app_new import db
from app_new.core.models.college import Building, HousingUnit
from app_new.core.models.planning import (
    Meter, MeterReading, MeterReadingCorrection, MeterAlert, MeterAlertEvidence,
    MeterAlertRule, MeterHeatingRegime, GasConversion, MeterTariff,
)
from app_new.core.services.meter_service import create_meter, record_meter_reading, correct_meter_reading
from app_new.core.services.meter_analytics import (
    consumption_intervals, aggregate_intervals, evaluate_alert_rule,
    statistical_anomaly, remainder_for_interval, estimated_cost, gas_coefficient,
    create_bill_gas_conversion, close_meter_alert,
)


def _meter(name, kind="eau", unit="m³"):
    building = Building(name=f"TEST_UI_C3_{name}")
    db.session.add(building)
    db.session.flush()
    return create_meter(name=f"TEST_UI_C3_{name}", meter_type=kind, unit=unit, building_id=building.id)


def _reading(meter, value, day, user_id):
    return record_meter_reading(meter=meter, value=value, user_id=user_id, reading_date=datetime(day.year, day.month, day.day, tzinfo=timezone.utc))


def test_c3_intervals_reset_and_calendar_day(app, admin_user):
    with app.app_context():
        meter = _meter("INTERVAL")
        _reading(meter, 100, date(2026, 1, 1), admin_user["id"])
        _reading(meter, 130, date(2026, 1, 11), admin_user["id"])
        # Le service C1 exige le reset explicite pour une baisse.
        _reading(meter, 5, date(2026, 1, 12), admin_user["id"]) if False else None
        values = consumption_intervals(meter)
        assert len(values) == 1 and values[0].raw_delta == 30 and values[0].calendar_days == 10
        assert aggregate_intervals(values, "MONTH")[0]["consumption"] == 30


def test_c3_context_heating_and_gas_tariff_cost(app, admin_user):
    with app.app_context():
        meter = _meter("GAS", "gaz", "m³")
        db.session.add(MeterHeatingRegime(meter=meter, network_name="TEST_UI_C3_RESEAU_A", regime="STOP", valid_from=date(2026, 1, 1), valid_to=date(2026, 1, 31)))
        db.session.add(GasConversion(meter=meter, coefficient_kwh_per_m3=10.85, valid_from=date(2026, 1, 1), origin="MANUAL"))
        db.session.add(MeterTariff(meter=meter, energy_type="gaz", unit_price=0.12, unit="€/kWh", valid_from=date(2026, 1, 1)))
        db.session.commit()
        _reading(meter, 1000, date(2026, 1, 1), admin_user["id"])
        _reading(meter, 1100, date(2026, 1, 11), admin_user["id"])
        interval = consumption_intervals(meter)[0]
        assert interval.context["heating"]["STOP"] == 10
        assert gas_coefficient(meter, date(2026, 1, 5)) == 10.85
        assert estimated_cost(meter, interval) == pytest.approx(100 * 10.85 * .12)


def test_c3_manual_alert_is_persisted_and_deduplicated(app, admin_user):
    with app.app_context():
        meter = _meter("ALERT")
        _reading(meter, 10, date(2026, 1, 1), admin_user["id"])
        _reading(meter, 30, date(2026, 1, 2), admin_user["id"])
        rule = MeterAlertRule(meter=meter, metric="consumption_per_day", period="DAY", operator=">", threshold=5, level="WARNING")
        db.session.add(rule); db.session.commit()
        interval = consumption_intervals(meter)[0]
        first = evaluate_alert_rule(rule, interval)
        second = evaluate_alert_rule(rule, interval)
        assert first.id == second.id and len(first.evidence) == 1


def test_c3_statistical_median_minimum_and_parent_remainder(app, admin_user):
    with app.app_context():
        parent = _meter("PRIMARY")
        child = _meter("SUB")
        child.parent = parent
        db.session.commit()
        for index, day in enumerate((date(2025, 1, 1), date(2025, 1, 2), date(2025, 1, 3), date(2025, 1, 4))):
            _reading(parent, index * 10, day, admin_user["id"])
            _reading(child, index * 2, day, admin_user["id"])
        intervals = consumption_intervals(parent)
        remainder = remainder_for_interval(parent, intervals[-1])
        assert remainder["quality"] == "EXACT" and remainder["value"] == 8
        assert statistical_anomaly(parent, intervals[-1], min_intervals=3) is None


def test_c3_monitoring_and_analytics_http(app, admin_user, authenticated_client):
    with app.app_context():
        meter = _meter("HTTP")
        _reading(meter, 1, date(2026, 2, 1), admin_user["id"])
        _reading(meter, 4, date(2026, 2, 4), admin_user["id"])
        meter_id = meter.id
    dashboard = authenticated_client.get("/planning/meter-monitoring")
    assert dashboard.status_code == 200 and b"Surveillance des compteurs" in dashboard.data


def test_c3_water_never_uses_gas_conversion_and_bill_conversion(app, admin_user):
    with app.app_context():
        water = _meter("WATER_COST", "eau", "m³")
        db.session.add(MeterTariff(meter=water, energy_type="eau", unit_price=4.20, unit="€/m³", valid_from=date(2026, 1, 1)))
        _reading(water, 20, date(2026, 1, 1), admin_user["id"]); _reading(water, 40, date(2026, 1, 2), admin_user["id"])
        db.session.commit()
        assert estimated_cost(water, consumption_intervals(water)[0]) == pytest.approx(84)
        gas = _meter("BILL", "gaz", "m³")
        conversion = create_bill_gas_conversion(meter=gas, volume_m3=1000, billed_kwh=10850, valid_from=date(2026, 1, 1))
        assert conversion.coefficient_kwh_per_m3 == pytest.approx(10.85)


def test_c3_alert_periods_and_zero_low_are_real(app, admin_user):
    with app.app_context():
        meter = _meter("PERIODS")
        days = [date(2026, 1, 1) + __import__('datetime').timedelta(days=i) for i in range(8)]
        for i, day in enumerate(days): _reading(meter, i * 10, day, admin_user["id"])
        intervals = consumption_intervals(meter)
        for period in ("DAY", "WEEK", "MONTH"):
            rule = MeterAlertRule(meter=meter, metric="consumption_interval", period=period, operator=">=", threshold=1, level="WARNING")
            db.session.add(rule); db.session.commit()
            assert evaluate_alert_rule(rule, intervals[-1], intervals=intervals) is not None
        inactive = MeterAlertRule(meter=meter, metric="consumption_interval", period="MONTH", operator=">", threshold=1, is_active=False)
        db.session.add(inactive); db.session.commit()
        assert evaluate_alert_rule(inactive, intervals[-1], intervals=intervals) is None
        low = MeterAlertRule(meter=meter, metric="consumption_per_day", period="DAY", operator=">", threshold=999, detect_zero_or_low=True, low_consumption_threshold=11)
        db.session.add(low); db.session.commit()
        assert evaluate_alert_rule(low, intervals[-1], intervals=intervals) is not None


def test_c3_alert_closure_excludes_only_relevant_baselines(app, admin_user):
    with app.app_context():
        meter = _meter("CLOSE")
        _reading(meter, 0, date(2026, 1, 1), admin_user["id"]); _reading(meter, 10, date(2026, 1, 2), admin_user["id"])
        rule = MeterAlertRule(meter=meter, metric="consumption_interval", period="DAY", operator=">", threshold=1)
        db.session.add(rule); db.session.commit()
        alert = evaluate_alert_rule(rule, consumption_intervals(meter)[0])
        close_meter_alert(alert=alert, conclusion="READING_ERROR", comment="TEST_UI_C3 erreur")
        assert alert.status == "CLOSED" and alert.evidence[0].excluded_from_baseline is True


def test_c3_performance_measurement(app, admin_user, authenticated_client):
    with app.app_context():
        first_meter_id = None
        for index in range(4):
            meter = _meter(f"PERF_{index}")
            first_meter_id = first_meter_id or meter.id
            for offset in range(12):
                _reading(meter, offset * 5, date(2025, 1, 1) + __import__('datetime').timedelta(days=offset), admin_user["id"])
        perf_meter = db.session.get(Meter, first_meter_id)
        perf_intervals = consumption_intervals(perf_meter)
        perf_rule = MeterAlertRule(meter=perf_meter, metric="consumption_interval", period="DAY", operator=">", threshold=1)
        db.session.add(perf_rule)
        db.session.commit()
        count = {"value": 0}
        from sqlalchemy import event
        def before_cursor(*args): count["value"] += 1
        def measure(label, callback):
            count["value"] = 0
            started = perf_counter()
            callback()
            elapsed = perf_counter() - started
            print(f"C3_CLOSE_PERF {label}_seconds={elapsed:.4f} {label}_queries={count['value']}")
            return elapsed, count["value"]
        event.listen(db.engine, "before_cursor_execute", before_cursor)
        dashboard_time, dashboard_queries = measure("dashboard", lambda: authenticated_client.get("/planning/meter-monitoring"))
        analytics_time, analytics_queries = measure("analytics", lambda: authenticated_client.get(f"/planning/meter-analytics/{first_meter_id}"))
        alert_time, alert_queries = measure("alerts", lambda: evaluate_alert_rule(perf_rule, perf_intervals[-1], intervals=perf_intervals))
        day_time, day_queries = measure("my_day", lambda: authenticated_client.get("/planning/my-day"))
        event.remove(db.engine, "before_cursor_execute", before_cursor)
        assert dashboard_time >= 0 and analytics_time >= 0 and alert_time >= 0 and day_time >= 0


def test_c3_new_reading_runs_targeted_alert_analysis(app, admin_user):
    with app.app_context():
        meter = _meter("AUTO_READING")
        _reading(meter, 0, date(2026, 3, 1), admin_user["id"])
        _reading(meter, 1, date(2026, 3, 2), admin_user["id"])
        rule = MeterAlertRule(meter=meter, metric="consumption_per_day", period="DAY", operator=">", threshold=5)
        db.session.add(rule)
        db.session.commit()
        reading = _reading(meter, 10, date(2026, 3, 3), admin_user["id"])
        alert = MeterAlert.query.filter_by(meter_id=meter.id, rule_id=rule.id, status="OPEN").one()
        assert alert.evidence and alert.evidence[0].reading_end_id == reading.id
        assert MeterAlertEvidence.query.filter_by(alert_id=alert.id).count() == 1


def test_c3_correction_recalculates_neighbors_and_closes_false_alert(app, admin_user):
    with app.app_context():
        meter = _meter("AUTO_CORRECTION")
        _reading(meter, 0, date(2026, 4, 1), admin_user["id"])
        _reading(meter, 1, date(2026, 4, 2), admin_user["id"])
        rule = MeterAlertRule(meter=meter, metric="consumption_per_day", period="DAY", operator=">", threshold=5)
        db.session.add(rule)
        db.session.commit()
        high = _reading(meter, 20, date(2026, 4, 3), admin_user["id"])
        alert = MeterAlert.query.filter_by(meter_id=meter.id, rule_id=rule.id, status="OPEN").one()
        correct_meter_reading(reading=high, new_value=3, user_id=admin_user["id"], reason="TEST_UI_C3_CLOSE correction")
        db.session.expire_all()
        refreshed = db.session.get(MeterAlert, alert.id)
        assert MeterReadingCorrection.query.filter_by(reading_id=high.id).count() == 1
        assert refreshed.status == "CLOSED"
        assert all(evidence.excluded_from_baseline for evidence in refreshed.evidence)


def test_c3_alert_close_route_and_dashboard_summaries(app, admin_user, authenticated_client):
    with app.app_context():
        water = _meter("CLOSE_HTTP_WATER", "eau", "m³")
        _reading(water, 0, date.today(), admin_user["id"])
        _reading(water, 12, date.today(), admin_user["id"])
        rule = MeterAlertRule(meter=water, metric="consumption_interval", period="DAY", operator=">", threshold=1)
        db.session.add(rule)
        db.session.commit()
        alert = evaluate_alert_rule(rule, consumption_intervals(water)[0])
        alert_id = alert.id
        response = authenticated_client.post(
            f"/planning/meter-alerts/{alert_id}/close",
            data={"status": "CLOSED", "conclusion": "BAD_THRESHOLD", "comment": "TEST_UI_C3_CLOSE"},
        )
    assert response.status_code == 302
    with app.app_context():
        assert db.session.get(MeterAlert, alert_id).status == "CLOSED"
        from app_new.core.models.maintenance import Intervention
        before = Intervention.query.count()
        first = authenticated_client.post(f"/planning/meter-alerts/{alert_id}/create-intervention", data={})
        second = authenticated_client.post(f"/planning/meter-alerts/{alert_id}/create-intervention", data={})
        after = Intervention.query.count()
        assert first.status_code == 302 and second.status_code == 302 and after == before + 1
        created = db.session.get(MeterAlert, alert_id).intervention
        db.session.delete(created)
        db.session.commit()
    dashboard = authenticated_client.get("/planning/meter-monitoring")
    assert dashboard.status_code == 200
    assert b"Eau" in dashboard.data and b"m" in dashboard.data


def _stat_rule(meter, **kwargs):
    values = dict(
        meter=meter, rule_type="STATISTICAL_ANOMALY", metric="consumption_per_day",
        period="DAY", min_comparable_intervals=3, min_deviation_percent=30,
        operator=">", threshold=0, level="WARNING",
    )
    values.update(kwargs)
    return MeterAlertRule(**values)


def _normal_history(meter, user_id, start=date(2026, 5, 1), count=4):
    for offset in range(count):
        _reading(meter, offset * 10, start + __import__('datetime').timedelta(days=offset), user_id)


def test_c3_final_statistical_anomaly_runs_from_new_reading(app, admin_user):
    with app.app_context():
        meter = _meter("STAT_AUTO")
        _normal_history(meter, admin_user["id"], date(2026, 5, 4))
        rule = _stat_rule(meter)
        db.session.add(rule)
        db.session.commit()
        _reading(meter, 45, date(2026, 5, 8), admin_user["id"])
        alert = MeterAlert.query.filter_by(meter_id=meter.id, rule_id=rule.id, alert_type="STATISTICAL_ANOMALY", status="OPEN").one()
        assert alert.reference_value == pytest.approx(10)
        assert alert.observed_value == pytest.approx(15)
        assert alert.deviation_percent >= 30
        assert alert.comparable_count >= 3
        assert len(alert.evidence) == 1


def test_c3_final_small_variation_and_insufficient_history_do_not_alert(app, admin_user):
    with app.app_context():
        small = _meter("STAT_SMALL")
        _normal_history(small, admin_user["id"], date(2026, 6, 1))
        small_rule = _stat_rule(small)
        db.session.add(small_rule)
        db.session.commit()
        _reading(small, 41, date(2026, 6, 5), admin_user["id"])
        assert MeterAlert.query.filter_by(meter_id=small.id, alert_type="STATISTICAL_ANOMALY").count() == 0

        insufficient = _meter("STAT_INSUFFICIENT")
        _normal_history(insufficient, admin_user["id"], date(2026, 7, 1), count=3)
        rule = _stat_rule(insufficient)
        db.session.add(rule)
        db.session.commit()
        _reading(insufficient, 35, date(2026, 7, 4), admin_user["id"])
        assert MeterAlert.query.filter_by(meter_id=insufficient.id, alert_type="STATISTICAL_ANOMALY").count() == 0


def test_c3_final_custom_statistical_parameters(app, admin_user):
    with app.app_context():
        building = Building(name="TEST_UI_C3_STAT_CUSTOM_BUILDING")
        db.session.add(building)
        db.session.flush()
        housing = HousingUnit(name="TEST_UI_C3_STAT_CUSTOM_HOUSING", building_id=building.id)
        db.session.add(housing)
        db.session.flush()
        meter = create_meter(name="TEST_UI_C3_STAT_CUSTOM", meter_type="eau", unit="m³", housing_unit_id=housing.id)
        _normal_history(meter, admin_user["id"], date(2026, 8, 3), count=5)
        rule = _stat_rule(meter, min_comparable_intervals=5, min_deviation_percent=40)
        db.session.add(rule)
        db.session.commit()
        _reading(meter, 50, date(2026, 8, 8), admin_user["id"])
        assert MeterAlert.query.filter_by(meter_id=meter.id, alert_type="STATISTICAL_ANOMALY").count() == 0
        _reading(meter, 65, date(2026, 8, 9), admin_user["id"])
        alert = MeterAlert.query.filter_by(meter_id=meter.id, alert_type="STATISTICAL_ANOMALY", status="OPEN").one()
        assert alert.comparable_count >= 5 and alert.deviation_percent >= 40


def test_c3_final_statistical_correction_normal_and_still_anomalous(app, admin_user):
    with app.app_context():
        meter = _meter("STAT_CORRECTION")
        _normal_history(meter, admin_user["id"], date(2026, 9, 7))
        rule = _stat_rule(meter)
        db.session.add(rule)
        db.session.commit()
        high = _reading(meter, 45, date(2026, 9, 11), admin_user["id"])
        original = MeterAlert.query.filter_by(meter_id=meter.id, rule_id=rule.id, status="OPEN").one()
        correct_meter_reading(reading=high, new_value=41, user_id=admin_user["id"], reason="TEST_UI_C3_FINAL normal")
        db.session.expire_all()
        assert db.session.get(MeterAlert, original.id).status == "CLOSED"

        high_again = db.session.get(MeterReading, high.id)
        correct_meter_reading(reading=high_again, new_value=50, user_id=admin_user["id"], reason="TEST_UI_C3_FINAL still anomalous")
        open_alerts = MeterAlert.query.filter_by(meter_id=meter.id, rule_id=rule.id, alert_type="STATISTICAL_ANOMALY", status="OPEN").all()
        assert len(open_alerts) == 1 and len(open_alerts[0].evidence) == 1


def test_c3_final_reading_recalculation_performance(app, admin_user):
    with app.app_context():
        meter = _meter("STAT_PERF")
        _normal_history(meter, admin_user["id"], date(2026, 10, 1), count=30)
        manual = MeterAlertRule(meter=meter, metric="consumption_per_day", period="DAY", operator=">", threshold=999)
        statistical = _stat_rule(meter)
        db.session.add_all([manual, statistical])
        db.session.commit()
        from sqlalchemy import event
        queries = {"count": 0}
        def count_query(*args):
            queries["count"] += 1
        event.listen(db.engine, "before_cursor_execute", count_query)
        started = perf_counter()
        _reading(meter, 315, date(2026, 11, 1), admin_user["id"])
        elapsed = perf_counter() - started
        event.remove(db.engine, "before_cursor_execute", count_query)
        print(f"C3_FINAL_PERF reading_seconds={elapsed:.4f} reading_queries={queries['count']} intervals={len(consumption_intervals(meter))}")
        assert elapsed >= 0 and queries["count"] > 0
