from datetime import date, time
from time import perf_counter

import pytest
from sqlalchemy import event

from app_new import db
from app_new.core.models.college import Building, HousingUnit
from app_new.core.models.planning import (
    Meter, MeterReading, MeterReadingOccurrence, MeterReadingRoundOccurrence,
    MeterReadingSchedule, WorkSchedule,
)
from app_new.core.models.user import User
from app_new.core.models.rbac import Role, UserRole
from app_new.core.services.meter_reading_planning import (
    CANCELLED, TREATED_READING, TREATED_WITHOUT_READING, add_round_member,
    create_or_update_schedule, create_round,
    generate_occurrences,
    operational_date_for, record_occurrence_reading, record_occurrence_without_reading,
    MeterDomainError,
)
from app_new.core.services.meter_service import create_meter, replace_meter


def _meter(name, building=None, housing=None):
    if building is None:
        building = Building(name=f"TEST_UI_C2_{name}_BUILDING")
        db.session.add(building)
        db.session.flush()
    return create_meter(name=f"TEST_UI_C2_{name}", building_id=building.id if housing is None else None, housing_unit_id=housing.id if housing else None)


def _schedule(meter, admin_user, frequency, reference, **kwargs):
    return create_or_update_schedule(
        meter=meter, frequency=frequency, reference_date=reference,
        assigned_to_id=admin_user["id"], **kwargs,
    )


def _test_user(suffix):
    username = f"TEST_UI_C2_FIX_{suffix}"
    user = User(username=username, email=f"{username.lower()}@gmao.local", full_name=username, role="technicien", is_active=True)
    user.set_password("test-c2-fix-password")
    db.session.add(user)
    db.session.flush()
    role = Role.query.filter_by(slug="technicien", is_active=True).first()
    db.session.add(UserRole(user_id=user.id, role_id=role.id))
    db.session.flush()
    return user


def test_c2_frequencies_and_generation_are_idempotent(app, admin_user):
    with app.app_context():
        frequencies = {
            "WEEKLY": date(2026, 10, 5), "MONTHLY": date(2026, 10, 1),
            "SEMI_ANNUAL": date(2026, 10, 1), "ANNUAL": date(2026, 10, 1),
            "FIXED_ANNUAL_DATE": date(2026, 10, 1),
        }
        schedules = []
        for frequency, reference in frequencies.items():
            meter = _meter(f"FREQ_{frequency}")
            schedules.append(_schedule(meter, admin_user, frequency, reference, fixed_month=10, fixed_day=1))
        db.session.commit()
        first = generate_occurrences(start_date=date(2026, 10, 1), end_date=date(2027, 10, 10))
        counts = {schedule.id: MeterReadingOccurrence.query.filter_by(schedule_id=schedule.id).count() for schedule in schedules}
        second = generate_occurrences(start_date=date(2026, 10, 1), end_date=date(2027, 10, 10))
        assert first
        assert second == []
        assert all(MeterReadingOccurrence.query.filter_by(schedule_id=schedule.id).count() == counts[schedule.id] for schedule in schedules)
        assert counts[schedules[0].id] == 53
        assert counts[schedules[1].id] == 13
        assert counts[schedules[2].id] == 3
        assert counts[schedules[3].id] == 2
        assert counts[schedules[4].id] == 2


def test_c2_target_and_operational_dates_and_housing_calendar(app, admin_user):
    with app.app_context():
        building = Building(name="TEST_UI_C2_CALENDAR_BUILDING")
        housing = HousingUnit(name="TEST_UI_C2_HOUSING", building=building)
        db.session.add_all([building, housing])
        db.session.flush()
        school_meter = _meter("SCHOOL_DATE", building=building)
        schedule = _schedule(school_meter, admin_user, "FIXED_ANNUAL_DATE", date(2026, 10, 3))
        db.session.add(WorkSchedule(user_id=admin_user["id"], day_of_week=4, start_time=time(8), end_time=time(17), is_active=True))
        db.session.flush()
        generate_occurrences(start_date=date(2026, 10, 3), end_date=date(2026, 10, 3), schedule_ids=[schedule.id])
        occurrence = MeterReadingOccurrence.query.filter_by(schedule_id=schedule.id).one()
        assert occurrence.target_date == date(2026, 10, 3)
        assert occurrence.operational_date == date(2026, 10, 2)

        housing_meter = _meter("HOUSING_DATE", housing=housing)
        housing_schedule = _schedule(housing_meter, admin_user, "ANNUAL", date(2026, 10, 3))
        generate_occurrences(start_date=date(2026, 10, 3), end_date=date(2026, 10, 3), schedule_ids=[housing_schedule.id])
        housing_occurrence = MeterReadingOccurrence.query.filter_by(schedule_id=housing_schedule.id).one()
        assert housing_occurrence.operational_date == date(2026, 10, 3)


def test_c2_processing_with_reading_and_without_reading_never_creates_zero(app, admin_user):
    with app.app_context():
        meter = _meter("PROCESSING")
        schedule = _schedule(meter, admin_user, "MONTHLY", date(2026, 10, 1))
        generate_occurrences(start_date=date(2026, 10, 1), end_date=date(2026, 10, 1), schedule_ids=[schedule.id])
        occurrence = MeterReadingOccurrence.query.filter_by(schedule_id=schedule.id).one()
        processed = record_occurrence_reading(occurrence_id=occurrence.id, value=42, user_id=admin_user["id"])
        assert processed.status == TREATED_READING
        assert MeterReading.query.filter_by(meter_id=meter.id).count() == 1

        other = _meter("WITHOUT")
        other_schedule = _schedule(other, admin_user, "MONTHLY", date(2026, 10, 1))
        generate_occurrences(start_date=date(2026, 10, 1), end_date=date(2026, 10, 1), schedule_ids=[other_schedule.id])
        other_occurrence = MeterReadingOccurrence.query.filter_by(schedule_id=other_schedule.id).one()
        without = record_occurrence_without_reading(
            occurrence_id=other_occurrence.id, reason="OCCUPANT_REFUSED", user_id=admin_user["id"], comment="TEST_UI_C2 refus"
        )
        assert without.status == TREATED_WITHOUT_READING
        assert without.comment == "TEST_UI_C2 refus"
        assert MeterReading.query.filter_by(meter_id=other.id).count() == 0


def test_c2_http_occurrence_actions_use_shared_service(app, admin_user, authenticated_client):
    with app.app_context():
        meter = _meter("HTTP_READING")
        schedule = _schedule(meter, admin_user, "MONTHLY", date(2026, 10, 1))
        generate_occurrences(start_date=date(2026, 10, 1), end_date=date(2026, 10, 1), schedule_ids=[schedule.id])
        occurrence = MeterReadingOccurrence.query.filter_by(schedule_id=schedule.id).one()

        response = authenticated_client.post(
            f"/planning/meter-occurrences/{occurrence.id}/reading",
            data={"value": "123.5", "notes": "TEST_UI_C2_HTTP"},
        )
        assert response.status_code == 302
        db.session.refresh(occurrence)
        assert occurrence.status == TREATED_READING
        assert MeterReading.query.filter_by(meter_id=meter.id).count() == 1

        other = _meter("HTTP_WITHOUT")
        other_schedule = _schedule(other, admin_user, "MONTHLY", date(2026, 10, 1))
        generate_occurrences(start_date=date(2026, 10, 1), end_date=date(2026, 10, 1), schedule_ids=[other_schedule.id])
        other_occurrence = MeterReadingOccurrence.query.filter_by(schedule_id=other_schedule.id).one()
        response = authenticated_client.post(
            f"/planning/meter-occurrences/{other_occurrence.id}/without-reading",
            data={"reason": "READING_UNKNOWN", "comment": "TEST_UI_C2_HTTP inconnu"},
        )
        assert response.status_code == 302
        db.session.refresh(other_occurrence)
        assert other_occurrence.status == TREATED_WITHOUT_READING
        assert other_occurrence.no_reading_reason == "READING_UNKNOWN"
        assert MeterReading.query.filter_by(meter_id=other.id).count() == 0


def test_c2_open_occurrence_stays_visible_as_overdue_in_my_day(app, admin_user, authenticated_client):
    with app.app_context():
        target = date(2026, 10, 5)
        meter = _meter("MY_DAY")
        _schedule(meter, admin_user, "WEEKLY", date(2026, 9, 28), duration_minutes=5)
        db.session.add(WorkSchedule(user_id=admin_user["id"], day_of_week=0, start_time=time(8), end_time=time(17), is_active=True))
        db.session.commit()
        response = authenticated_client.get(f"/planning/my-day?date={target.isoformat()}")
        assert response.status_code == 200
        assert b"Relev\xc3\xa9 compteur" in response.data
        assert b"Saisir le relev\xc3\xa9" in response.data


def test_c2_replaced_meter_cancels_open_occurrences(app, admin_user):
    with app.app_context():
        meter = _meter("REPLACED")
        schedule = _schedule(meter, admin_user, "MONTHLY", date(2026, 10, 1))
        generate_occurrences(start_date=date(2026, 10, 1), end_date=date(2026, 10, 1), schedule_ids=[schedule.id])
        old_occurrence = MeterReadingOccurrence.query.filter_by(schedule_id=schedule.id).one()
        replace_meter(old_meter=meter, name="TEST_UI_C2_REPLACEMENT", user_id=admin_user["id"])
        generate_occurrences(start_date=date(2026, 10, 1), end_date=date(2027, 1, 1))
        db.session.refresh(old_occurrence)
        assert old_occurrence.status == CANCELLED
        assert MeterReadingOccurrence.query.filter_by(schedule_id=schedule.id, status="À FAIRE").count() == 0


def test_c2_round_order_progress_and_resume(app, admin_user, authenticated_client):
    with app.app_context():
        first = _meter("ROUND_1")
        second = _meter("ROUND_2")
        first_schedule = _schedule(first, admin_user, "MONTHLY", date(2026, 10, 1))
        second_schedule = _schedule(second, admin_user, "MONTHLY", date(2026, 10, 1))
        round_ = create_round(name="TEST_UI_C2_TOURNEE", default_assigned_to_id=admin_user["id"])
        add_round_member(round_=round_, schedule=first_schedule, position=1, commit=False)
        add_round_member(round_=round_, schedule=second_schedule, position=2, commit=False)
        db.session.commit()
        generate_occurrences(start_date=date(2026, 10, 1), end_date=date(2026, 10, 1))
        round_occurrence = MeterReadingRoundOccurrence.query.filter_by(round_id=round_.id, operational_date=date(2026, 10, 1)).one()
        assert [item.schedule.meter.name for item in sorted(round_occurrence.occurrences, key=lambda item: item.schedule.meter.name)]
        response = authenticated_client.get(f"/planning/meter-rounds/{round_.id}?date=2026-10-01")
        assert response.status_code == 200
        assert b"TEST_UI_C2_TOURNEE" in response.data
        assert b"0 / 2" in response.data
        record_occurrence_without_reading(
            occurrence_id=round_occurrence.occurrences[0].id, reason="READING_UNKNOWN", user_id=admin_user["id"]
        )
        db.session.refresh(round_occurrence)
        assert round_occurrence.status == "EN COURS"
        record_occurrence_reading(
            occurrence_id=round_occurrence.occurrences[1].id, value=10, user_id=admin_user["id"]
        )
        db.session.refresh(round_occurrence)
        assert round_occurrence.status == "TERMINÉE"
        db.session.expire_all()
        response = authenticated_client.get(f"/planning/meter-rounds/{round_.id}?date=2026-10-01")
        assert response.status_code == 200
        assert b"2 / 2" in response.data
        assert response.data.index(b"ROUND_1") < response.data.index(b"ROUND_2")


def test_c2_fix_direct_round_generates_future_target_on_operational_date(app, admin_user, authenticated_client):
    with app.app_context():
        meter = _meter("DIRECT_OPERATIONAL_DATE")
        schedule = create_or_update_schedule(
            meter=meter, frequency="FIXED_ANNUAL_DATE", reference_date=date(2023, 10, 1),
            fixed_month=10, fixed_day=1, assigned_to_id=None,
        )
        db.session.add(WorkSchedule(user_id=admin_user["id"], day_of_week=4, start_time=time(8), end_time=time(17), is_active=True))
        round_ = create_round(name="TEST_UI_C2_FIX_DIRECT_ROUND", default_assigned_to_id=admin_user["id"])
        add_round_member(round_=round_, schedule=schedule, position=1)
        db.session.commit()

        # Aucun appel préalable à Ma journée ni génération couvrant le 01/10.
        assert MeterReadingOccurrence.query.filter_by(schedule_id=schedule.id).count() == 0
        response = authenticated_client.get(f"/planning/meter-rounds/{round_.id}?date=2023-09-29")
        assert response.status_code == 200
        assert b"TEST_UI_C2_DIRECT_OPERATIONAL_DATE" in response.data
        occurrence = MeterReadingOccurrence.query.filter_by(schedule_id=schedule.id).one()
        assert occurrence.target_date == date(2023, 10, 1)
        assert occurrence.operational_date == date(2023, 9, 29)
        assert occurrence.round_occurrence.round_id == round_.id
        assert occurrence.assigned_to_id == admin_user["id"]
        second_response = authenticated_client.get(f"/planning/meter-rounds/{round_.id}?date=2023-09-29")
        assert second_response.status_code == 200
        assert MeterReadingOccurrence.query.filter_by(schedule_id=schedule.id).count() == 1


def test_c2_fix_schedule_assignment_precedes_round_default(app, admin_user):
    with app.app_context():
        individual = _test_user("INDIVIDUAL")
        meter = _meter("ASSIGNMENT_PRIORITY")
        schedule = create_or_update_schedule(
            meter=meter, frequency="MONTHLY", reference_date=date(2026, 10, 1),
            assigned_to_id=individual.id,
        )
        round_ = create_round(name="TEST_UI_C2_FIX_PRIORITY", default_assigned_to_id=admin_user["id"])
        add_round_member(round_=round_, schedule=schedule, position=1)
        db.session.commit()
        generate_occurrences(start_date=date(2026, 10, 1), end_date=date(2026, 10, 1), schedule_ids=[schedule.id])
        occurrence = MeterReadingOccurrence.query.filter_by(schedule_id=schedule.id).one()
        assert occurrence.assigned_to_id == individual.id


def test_c2_fix_double_processing_is_rejected_without_duplicate_reading(app, admin_user):
    with app.app_context():
        meter = _meter("DOUBLE_PROCESSING")
        schedule = _schedule(meter, admin_user, "MONTHLY", date(2026, 10, 1))
        generate_occurrences(start_date=date(2026, 10, 1), end_date=date(2026, 10, 1), schedule_ids=[schedule.id])
        occurrence = MeterReadingOccurrence.query.filter_by(schedule_id=schedule.id).one()
        record_occurrence_reading(occurrence_id=occurrence.id, value=5, user_id=admin_user["id"])
        with pytest.raises(MeterDomainError, match="déjà traitée ou annulée"):
            record_occurrence_reading(occurrence_id=occurrence.id, value=6, user_id=admin_user["id"])
        assert MeterReading.query.filter_by(meter_id=meter.id).count() == 1

        other = _meter("DOUBLE_WITHOUT")
        other_schedule = _schedule(other, admin_user, "MONTHLY", date(2026, 10, 1))
        generate_occurrences(start_date=date(2026, 10, 1), end_date=date(2026, 10, 1), schedule_ids=[other_schedule.id])
        other_occurrence = MeterReadingOccurrence.query.filter_by(schedule_id=other_schedule.id).one()
        record_occurrence_without_reading(occurrence_id=other_occurrence.id, reason="INACCESSIBLE", user_id=admin_user["id"])
        with pytest.raises(MeterDomainError, match="déjà traitée ou annulée"):
            record_occurrence_reading(occurrence_id=other_occurrence.id, value=9, user_id=admin_user["id"])
        assert MeterReading.query.filter_by(meter_id=other.id).count() == 0


def test_c2_fix_user_isolation_http(app):
    with app.app_context():
        user_a = _test_user("USER_A")
        user_b = _test_user("USER_B")
        meter = _meter("USER_ISOLATION")
        schedule = create_or_update_schedule(
            meter=meter, frequency="MONTHLY", reference_date=date(2026, 10, 1), assigned_to_id=user_a.id,
        )
        db.session.commit()
        generate_occurrences(start_date=date(2026, 10, 1), end_date=date(2026, 10, 1), schedule_ids=[schedule.id])
        occurrence = MeterReadingOccurrence.query.filter_by(schedule_id=schedule.id).one()
        client = app.test_client()
        response = client.post('/auth/login', data={'username': user_b.username, 'password': 'test-c2-fix-password'})
        assert response.status_code == 302
        assert client.get(f"/planning/meter-occurrences/{occurrence.id}").status_code == 403
        assert client.post(f"/planning/meter-occurrences/{occurrence.id}/reading", data={'value': '12'}).status_code == 302
        db.session.refresh(occurrence)
        assert occurrence.status == "À FAIRE"
        client.get('/auth/logout')
        response = client.post('/auth/login', data={'username': user_a.username, 'password': 'test-c2-fix-password'})
        assert response.status_code == 302
        assert client.post(f"/planning/meter-occurrences/{occurrence.id}/without-reading", data={'reason': 'READING_UNKNOWN'}).status_code == 302
        db.session.refresh(occurrence)
        assert occurrence.status == TREATED_WITHOUT_READING


def test_c2_permissions_keep_configuration_admin_only(app, client):
    response = client.get('/planning/meter-schedules/new')
    assert response.status_code in (302, 403)


def test_c2_planning_sql_smoke_is_measured(app, admin_user, authenticated_client):
    with app.app_context():
        schedules = []
        for index in range(3):
            meter = _meter(f"PERF_{index}")
            schedules.append(_schedule(meter, admin_user, "MONTHLY", date(2026, 10, 1)))
        round_ = create_round(name="TEST_UI_C2_PERF_ROUND", default_assigned_to_id=admin_user["id"])
        for position, schedule in enumerate(schedules, 1):
            add_round_member(round_=round_, schedule=schedule, position=position, commit=False)
        db.session.commit()

        query_count = 0

        def count_queries(*_args, **_kwargs):
            nonlocal query_count
            query_count += 1

        event.listen(db.engine, "before_cursor_execute", count_queries)
        try:
            start = perf_counter()
            daily = authenticated_client.get("/planning/my-day?date=2026-10-01")
            daily_seconds = perf_counter() - start
            daily_queries = query_count
            query_count = 0
            start = perf_counter()
            round_response = authenticated_client.get(
                f"/planning/meter-rounds/{round_.id}?date=2026-10-01"
            )
            round_seconds = perf_counter() - start
            round_queries = query_count
        finally:
            event.remove(db.engine, "before_cursor_execute", count_queries)

        print(
            f"C2_PERF my_day_seconds={daily_seconds:.4f} my_day_queries={daily_queries} "
            f"round_seconds={round_seconds:.4f} round_queries={round_queries}"
        )
        assert daily.status_code == 200
        assert round_response.status_code == 200
