from datetime import date

import pytest

from ccma.services.housekeeper import Housekeeper
from ccma.storage.repository import MemberRepository, RepositoryError


def _member(tmp_path):
    repository = MemberRepository(tmp_path)
    repository.initialize()
    member = repository.create_member(first_name="Override", last_name="Test", birth_date="2010-01-01")
    member.status = "active"
    member.accepted_at = "2026-01-01"
    member.membership_started_at = "2026-01-01"
    member.payment_frequency = "semiannual"
    repository.save_member(member)
    return repository, member


def test_contribution_override_can_be_recorded_with_amount(tmp_path) -> None:
    repository, member = _member(tmp_path)

    override = repository.record_contribution_override(
        member.member_id,
        valid_from="03.2026",
        valid_until="",
        kind="amount",
        value="90.00",
        reason="Schüler laut Nachweis",
    )

    data = repository.get_contributions(member.member_id)
    assert data.contribution_overrides == [override]
    assert override["valid_from"] == "2026-03"
    assert override["valid_until"] == ""
    assert override["kind"] == "amount"
    assert override["value"] == "90.00"
    assert override["reason"] == "Schüler laut Nachweis"
    assert repository.get_events(member.member_id)[-1].event_type == "contribution_override_recorded"


def test_contribution_override_accepts_percent_kind(tmp_path) -> None:
    repository, member = _member(tmp_path)

    override = repository.record_contribution_override(
        member.member_id,
        valid_from="2026-03",
        valid_until="2026-08",
        kind="percent",
        value="50",
        reason="Schüler laut Nachweis",
    )

    assert override["kind"] == "percent"
    assert override["value"] == "50.00"
    assert override["valid_until"] == "2026-08"


def test_contribution_override_requires_reason(tmp_path) -> None:
    repository, member = _member(tmp_path)

    with pytest.raises(RepositoryError, match="Begründung"):
        repository.record_contribution_override(
            member.member_id, valid_from="03.2026", kind="amount", value="90.00", reason="   "
        )


def test_contribution_override_rejects_invalid_percent(tmp_path) -> None:
    repository, member = _member(tmp_path)

    with pytest.raises(RepositoryError, match="zwischen 0 und 100"):
        repository.record_contribution_override(
            member.member_id, valid_from="03.2026", kind="percent", value="150", reason="Test"
        )
    with pytest.raises(RepositoryError, match="zwischen 0 und 100"):
        repository.record_contribution_override(
            member.member_id, valid_from="03.2026", kind="percent", value="0", reason="Test"
        )


def test_contribution_override_rejects_negative_amount(tmp_path) -> None:
    repository, member = _member(tmp_path)

    with pytest.raises(RepositoryError, match="nicht negativ"):
        repository.record_contribution_override(
            member.member_id, valid_from="03.2026", kind="amount", value="-10", reason="Test"
        )


def test_contribution_override_rejects_until_before_from(tmp_path) -> None:
    repository, member = _member(tmp_path)

    with pytest.raises(RepositoryError, match="darf nicht vor"):
        repository.record_contribution_override(
            member.member_id,
            valid_from="08.2026",
            valid_until="03.2026",
            kind="amount",
            value="90.00",
            reason="Test",
        )


def test_overlapping_contribution_overrides_are_rejected(tmp_path) -> None:
    repository, member = _member(tmp_path)
    repository.record_contribution_override(
        member.member_id,
        valid_from="2026-01",
        valid_until="2026-06",
        kind="amount",
        value="90.00",
        reason="Erstes",
    )

    with pytest.raises(RepositoryError, match="überschneidet sich"):
        repository.record_contribution_override(
            member.member_id,
            valid_from="2026-04",
            valid_until="2026-12",
            kind="amount",
            value="100.00",
            reason="Zweites",
        )

    # Adjacent, non-overlapping range is fine.
    second = repository.record_contribution_override(
        member.member_id,
        valid_from="2026-07",
        valid_until="",
        kind="amount",
        value="100.00",
        reason="Zweites, ab Juli",
    )
    assert second["valid_from"] == "2026-07"


def test_open_ended_override_overlaps_with_any_later_range(tmp_path) -> None:
    repository, member = _member(tmp_path)
    repository.record_contribution_override(
        member.member_id, valid_from="2026-01", kind="amount", value="90.00", reason="Unbefristet"
    )

    with pytest.raises(RepositoryError, match="überschneidet sich"):
        repository.record_contribution_override(
            member.member_id,
            valid_from="2030-01",
            valid_until="2030-06",
            kind="amount",
            value="100.00",
            reason="Später",
        )


def test_contribution_override_can_be_updated_and_deleted(tmp_path) -> None:
    repository, member = _member(tmp_path)
    override = repository.record_contribution_override(
        member.member_id, valid_from="2026-01", kind="amount", value="90.00", reason="Erstversion"
    )

    updated = repository.update_contribution_override(
        member.member_id,
        override["override_id"],
        valid_from="2026-02",
        valid_until="2026-12",
        kind="percent",
        value="40",
        reason="Korrigiert",
    )
    assert updated["valid_from"] == "2026-02"
    assert updated["kind"] == "percent"
    assert updated["reason"] == "Korrigiert"
    assert repository.get_events(member.member_id)[-1].event_type == "contribution_override_changed"

    repository.delete_contribution_override(member.member_id, override["override_id"])
    data = repository.get_contributions(member.member_id)
    assert data.contribution_overrides == []
    assert repository.get_events(member.member_id)[-1].event_type == "contribution_override_deleted"


def test_updating_override_can_keep_its_own_range(tmp_path) -> None:
    repository, member = _member(tmp_path)
    override = repository.record_contribution_override(
        member.member_id,
        valid_from="2026-01",
        valid_until="2026-06",
        kind="amount",
        value="90.00",
        reason="Erstversion",
    )

    # Saving again with the same range must not trip the overlap check against itself.
    updated = repository.update_contribution_override(
        member.member_id,
        override["override_id"],
        valid_from="2026-01",
        valid_until="2026-06",
        kind="amount",
        value="95.00",
        reason="Betrag korrigiert",
    )
    assert updated["value"] == "95.00"


def _housekeeper_member(tmp_path, *, payment_frequency="annual", started_at="2025-01-01"):
    repository = MemberRepository(tmp_path)
    repository.initialize()
    member = repository.create_member(first_name="Override", last_name="Claims", birth_date="1990-01-01")
    member.status = "active"
    member.accepted_at = started_at
    member.membership_started_at = started_at
    member.payment_frequency = payment_frequency
    repository.save_member(member)
    return repository, member


def test_full_year_amount_override_replaces_the_annual_claim_amount(tmp_path) -> None:
    repository, member = _housekeeper_member(tmp_path, payment_frequency="annual")
    repository.record_contribution_override(
        member.member_id,
        valid_from="2026-01",
        valid_until="2026-12",
        kind="amount",
        value="60.00",
        reason="Schüler laut Nachweis",
    )

    Housekeeper(repository).run(today=date(2026, 6, 21))

    claims_by_key = {
        claim["claim_key"]: claim for claim in repository.get_contributions(member.member_id).claims
    }
    claim = claims_by_key["membership-fee:2026:annual"]
    assert claim["amount"] == "60.00"
    assert claim["calculation"]["contribution_override_ids"] != []


def test_full_year_percent_override_reduces_the_base_rate(tmp_path) -> None:
    repository, member = _housekeeper_member(tmp_path, payment_frequency="annual")
    repository.record_contribution_override(
        member.member_id,
        valid_from="2026-01",
        valid_until="2026-12",
        kind="percent",
        value="50",
        reason="Schüler laut Nachweis",
    )

    Housekeeper(repository).run(today=date(2026, 6, 21))

    claim = next(
        claim
        for claim in repository.get_contributions(member.member_id).claims
        if claim["claim_key"] == "membership-fee:2026:annual"
    )
    assert claim["amount"] == "75.00"


def test_override_starting_mid_period_blends_month_by_month(tmp_path) -> None:
    repository, member = _housekeeper_member(tmp_path, payment_frequency="semiannual")
    repository.record_contribution_override(
        member.member_id,
        valid_from="2026-03",
        valid_until="2026-08",
        kind="amount",
        value="60.00",
        reason="Schüler ab März",
    )

    Housekeeper(repository).run(today=date(2026, 12, 31))

    claims_by_key = {
        claim["claim_key"]: claim for claim in repository.get_contributions(member.member_id).claims
    }
    # Jan+Feb at 12.50/month (full rate) + Mar-Jun at 5.00/month (override) = 45.00
    assert claims_by_key["membership-fee:2026:first-half"]["amount"] == "45.00"
    # Jul+Aug at 5.00/month (override) + Sep-Dec at 12.50/month (full rate) = 60.00
    assert claims_by_key["membership-fee:2026:second-half"]["amount"] == "60.00"


def test_claim_without_applicable_override_is_unaffected(tmp_path) -> None:
    repository, member = _housekeeper_member(tmp_path, payment_frequency="annual")
    repository.record_contribution_override(
        member.member_id,
        valid_from="2020-01",
        valid_until="2020-12",
        kind="amount",
        value="60.00",
        reason="Frueherer Zeitraum",
    )

    Housekeeper(repository).run(today=date(2026, 6, 21))

    claim = next(
        claim
        for claim in repository.get_contributions(member.member_id).claims
        if claim["claim_key"] == "membership-fee:2026:annual"
    )
    assert claim["amount"] == "150.00"
    assert claim["calculation"]["contribution_override_ids"] == []
