import json
import shutil
from datetime import date
from decimal import Decimal

import pytest

import ccma.services.housekeeper as housekeeper_module
from ccma.rules.loader import RuleLoadError
from ccma.services.housekeeper import Housekeeper
from ccma.storage.repository import MemberRepository, RepositoryError


def test_housekeeper_optionally_waits_between_members(tmp_path, monkeypatch) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    repository.create_member(first_name="First", last_name="Member")
    repository.create_member(first_name="Second", last_name="Member")
    delays: list[float] = []
    monkeypatch.setattr(housekeeper_module.time, "sleep", delays.append)

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

    assert delays == [0.25]


def test_store_rule_overrides_builtin_rule_with_same_filename(tmp_path) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    member = repository.create_member(first_name="Override", last_name="Test")
    (repository.root / "rules" / "birthdate_check.py").write_text(
        """
from ccma.rules.api import RuleAction

RULE_ID = "birthdate-check"

def evaluate(context):
    return [RuleAction(
        key=f"birthdate-check:{context.member.member_id}:override",
        action="task",
        member_id=context.member.member_id,
        payload={
            "code": "override_active",
            "severity": "info",
            "title": "Store-Override aktiv",
            "detail": "Die eingebaute Regel wurde ersetzt.",
        },
    )]
""".strip(),
        encoding="utf-8",
    )

    findings = Housekeeper(repository).run(today=date(2026, 6, 21))
    state = json.loads((repository.root / "housekeeper.json").read_text(encoding="utf-8"))

    assert not (repository.root / "hausmeister.json").exists()
    assert not (repository.root / ".hausmeister.lock").exists()
    assert any(finding.code == "override_active" for finding in findings)
    rule = next(item for item in state["rules"] if item["filename"] == "birthdate_check.py")
    assert rule["source"] == "store-override"
    assert rule["script_hash"].startswith("sha256:")
    assert member.member_id in {item["member_id"] for item in state["items"]}


def test_housekeeper_claim_actions_are_idempotent(tmp_path) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    member = repository.create_member(first_name="Contribution", last_name="Test", birth_date="1990-01-01")
    member.status = "active"
    member.accepted_at = "2026-04-15"
    member.membership_started_at = "2026-04-15"
    member.payment_frequency = "semiannual"
    repository.save_member(member)
    housekeeper = Housekeeper(repository)

    housekeeper.run(today=date(2026, 4, 15))
    first_claims = repository.get_contributions(member.member_id).claims
    housekeeper.run(today=date(2026, 4, 15))
    second_claims = repository.get_contributions(member.member_id).claims
    state = json.loads((repository.root / "housekeeper.json").read_text(encoding="utf-8"))

    assert {claim["claim_key"] for claim in first_claims} == {
        "admission-fee",
        "membership-fee:2026:first-half",
        "membership-fee:2026:second-half",
    }
    assert len(second_claims) == len(first_claims) == 3
    amounts = {claim["claim_key"]: claim["amount"] for claim in first_claims}
    assert amounts["membership-fee:2026:first-half"] == "37.50"
    assert amounts["membership-fee:2026:second-half"] == "75.00"
    descriptions = {
        claim["claim_key"]: claim["items"][0]["description"] for claim in first_claims
    }
    assert descriptions["membership-fee:2026:first-half"].endswith("(3 Monate)")
    assert descriptions["membership-fee:2026:second-half"] == "Mitgliedsbeitrag 2. Halbjahr 2026"
    assert state["run_counter"] == 2
    assert state["last_completed_run"] == "2026-04-15:000002"


@pytest.mark.parametrize(
    ("frequency", "expected_suffixes", "expected_amount"),
    [
        ("monthly", [f"month-{month:02d}" for month in range(1, 13)], "12.50"),
        ("quarterly", [f"quarter-{quarter}" for quarter in range(1, 5)], "37.50"),
        ("semiannual", ["first-half", "second-half"], "75.00"),
        ("annual", ["annual"], "150.00"),
    ],
)
def test_housekeeper_creates_claims_for_each_payment_frequency(
    tmp_path, frequency, expected_suffixes, expected_amount
) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    member = repository.create_member(
        first_name=frequency,
        last_name="Frequency",
        birth_date="1990-01-01",
    )
    member.status = "active"
    member.accepted_at = "2026-01-01"
    member.membership_started_at = "2026-01-01"
    member.payment_frequency = frequency
    repository.save_member(member)

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

    membership_claims = [
        claim
        for claim in repository.get_contributions(member.member_id).claims
        if claim.get("type") == "membership_fee"
    ]
    assert [claim["claim_key"].rsplit(":", 1)[-1] for claim in membership_claims] == expected_suffixes
    assert {claim["amount"] for claim in membership_claims} == {expected_amount}
    assert sum(Decimal(claim["amount"]) for claim in membership_claims) == Decimal("150.00")


def test_housekeeper_creates_membership_claims_retroactively_since_entry(tmp_path) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    member = repository.create_member(first_name="Retro", last_name="Claims", birth_date="1990-01-01")
    member.status = "active"
    member.accepted_at = "2024-04-15"
    member.membership_started_at = "2024-04-15"
    member.payment_frequency = "annual"
    repository.save_member(member)

    settings = housekeeper_module.HousekeeperSettings.from_values(
        birthday_days_before=0,
        birthday_days_after=0,
        anniversary_days_before=0,
        anniversary_days_after=0,
        anniversary_intervals="1Y",
        retroactive_claims=True,
    )

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

    claims = repository.get_contributions(member.member_id).claims
    claims_by_key = {claim["claim_key"]: claim for claim in claims}

    assert set(claims_by_key) == {
        "admission-fee",
        "membership-fee:2024:annual",
        "membership-fee:2025:annual",
        "membership-fee:2026:annual",
    }
    assert claims_by_key["membership-fee:2024:annual"]["amount"] == "112.50"
    assert claims_by_key["membership-fee:2024:annual"]["items"][0]["description"].endswith(
        "(9 Monate)"
    )


def test_housekeeper_uses_pre_2022_contribution_amounts_for_legacy_years(tmp_path) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    member = repository.create_member(first_name="Legacy", last_name="Rates", birth_date="1990-01-01")
    member.status = "active"
    member.accepted_at = "2021-04-15"
    member.membership_started_at = "2021-04-15"
    member.payment_frequency = "annual"
    repository.save_member(member)

    settings = housekeeper_module.HousekeeperSettings.from_values(
        birthday_days_before=0,
        birthday_days_after=0,
        anniversary_days_before=0,
        anniversary_days_after=0,
        anniversary_intervals="1Y",
        retroactive_claims=True,
    )

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

    claims = repository.get_contributions(member.member_id).claims
    claims_by_key = {claim["claim_key"]: claim for claim in claims}

    assert claims_by_key["membership-fee:2021:annual"]["amount"] == "90.00"
    assert claims_by_key["membership-fee:2022:annual"]["amount"] == "150.00"


def test_housekeeper_does_not_create_retroactive_membership_claims_by_default(tmp_path) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    member = repository.create_member(first_name="Current", last_name="Only", birth_date="1990-01-01")
    member.status = "active"
    member.accepted_at = "2024-04-15"
    member.membership_started_at = "2024-04-15"
    member.payment_frequency = "annual"
    repository.save_member(member)

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

    claim_keys = {claim["claim_key"] for claim in repository.get_contributions(member.member_id).claims}

    assert claim_keys == {
        "admission-fee",
        "membership-fee:2026:annual",
    }


def test_housekeeper_does_not_create_claims_due_after_membership_end(tmp_path) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    member = repository.create_member(first_name="Former", last_name="Member", birth_date="1990-01-01")
    member.status = "resigned_end_of_year"
    member.accepted_at = "2024-01-01"
    member.membership_started_at = "2024-01-01"
    member.membership_ended_at = "2026-12-31"
    member.payment_frequency = "annual"
    repository.save_member(member)

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

    claim_keys = {claim["claim_key"] for claim in repository.get_contributions(member.member_id).claims}
    assert "membership-fee:2026:annual" in claim_keys
    assert "membership-fee:2027:annual" not in claim_keys


def test_housekeeper_skips_birthday_and_anniversary_for_ended_members(tmp_path) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    active = repository.create_member(first_name="Active", last_name="Member", birth_date="1990-06-21")
    active.status = "active"
    active.accepted_at = "2025-06-21"
    active.membership_started_at = "2025-06-21"
    repository.save_member(active)
    ended = repository.create_member(first_name="Ended", last_name="Member", birth_date="1990-06-21")
    ended.status = "ended"
    ended.accepted_at = "2025-06-21"
    ended.membership_started_at = "2025-06-21"
    ended.membership_ended_at = "2025-12-31"
    repository.save_member(ended)

    settings = housekeeper_module.HousekeeperSettings.from_values(
        birthday_days_before=0,
        birthday_days_after=0,
        anniversary_days_before=0,
        anniversary_days_after=0,
        anniversary_intervals="1Y",
    )

    findings = Housekeeper(repository, settings).run(today=date(2026, 6, 21))
    codes_by_member = {}
    for item in findings:
        codes_by_member.setdefault(item.member_id, set()).add(item.code)

    assert "birthday" in codes_by_member.get(active.member_id, set())
    assert "membership_anniversary" in codes_by_member.get(active.member_id, set())
    assert codes_by_member.get(ended.member_id, set()).isdisjoint({"birthday", "membership_anniversary"})


def test_housekeeper_resolves_tasks_not_seen_in_current_run(tmp_path) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    member = repository.create_member(first_name="Missing", last_name="Birthday")
    housekeeper = Housekeeper(repository)

    housekeeper.run(today=date(2026, 6, 21))
    member.birth_date = "1990-01-01"
    repository.save_member(member)
    housekeeper.run(today=date(2026, 6, 21))
    state = json.loads((repository.root / "housekeeper.json").read_text(encoding="utf-8"))
    task = next(item for item in state["items"] if item["key"].endswith(":missing:birth_date"))

    assert task["status"] == "resolved"
    assert task["first_seen_run"] == "2026-06-21:000001"
    assert task["resolved_run"] == "2026-06-21:000002"


def test_failed_run_does_not_advance_persisted_run_id(tmp_path) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    repository.create_member(first_name="Failed", last_name="Rule")
    housekeeper = Housekeeper(repository)
    housekeeper.run(today=date(2026, 6, 21))
    state_before = (repository.root / "housekeeper.json").read_bytes()
    (repository.root / "rules" / "broken.py").write_text("this is not python !!!", encoding="utf-8")

    with pytest.raises(RuleLoadError):
        housekeeper.run(today=date(2026, 6, 21))

    assert (repository.root / "housekeeper.json").read_bytes() == state_before
    assert not (repository.root / ".housekeeper.lock").exists()


def test_broken_contributions_file_creates_task_without_overwriting_file(tmp_path) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    member = repository.create_member(first_name="Broken", last_name="Contributions", birth_date="1990-01-01")
    member.status = "active"
    member.accepted_at = "2026-01-01"
    member.membership_started_at = "2026-01-01"
    repository.save_member(member)
    path = repository.members_root / member.member_id / "contributions.json"
    path.write_bytes(b"")

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

    assert path.read_bytes() == b""
    assert [finding.code for finding in findings] == ["invalid_member_record"]
    state = json.loads((repository.root / "housekeeper.json").read_text(encoding="utf-8"))
    task = next(item for item in state["items"] if item["code"] == "invalid_member_record")
    assert task["status"] == "open"


def test_preflight_skips_all_rules_for_broken_member_file(tmp_path) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    member = repository.create_member(first_name="Broken", last_name="Member")
    member_path = repository.members_root / member.member_id / "member.json"
    contributions_path = repository.members_root / member.member_id / "contributions.json"
    contributions_before = contributions_path.read_bytes()
    member_path.write_text("{", encoding="utf-8")

    findings = Housekeeper(repository).run(today=date(2026, 6, 21))
    state = json.loads((repository.root / "housekeeper.json").read_text(encoding="utf-8"))

    assert [finding.code for finding in findings] == ["invalid_member_record"]
    assert len(state["items"]) == 1
    assert contributions_path.read_bytes() == contributions_before


def test_preflight_blocks_rules_for_broken_event_log(tmp_path) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    member = repository.create_member(first_name="Broken", last_name="Events", birth_date="1990-01-01")
    member.status = "active"
    member.accepted_at = "2026-01-01"
    member.membership_started_at = "2026-01-01"
    repository.save_member(member)
    events_path = repository.members_root / member.member_id / "events.jsonl"
    with events_path.open("a", encoding="utf-8") as handle:
        handle.write("not-json\n")

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

    assert [finding.code for finding in findings] == ["invalid_member_record"]
    assert repository.get_contributions(member.member_id).claims == []


def test_preflight_task_resolves_after_record_is_repaired(tmp_path) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    member = repository.create_member(first_name="Repair", last_name="Record", birth_date="1990-01-01")
    contributions_path = repository.members_root / member.member_id / "contributions.json"
    original = contributions_path.read_bytes()
    contributions_path.write_bytes(b"")
    housekeeper = Housekeeper(repository)
    housekeeper.run(today=date(2026, 6, 21))
    contributions_path.write_bytes(original)

    housekeeper.run(today=date(2026, 6, 21))
    state = json.loads((repository.root / "housekeeper.json").read_text(encoding="utf-8"))
    task = next(item for item in state["items"] if item["code"] == "invalid_member_record")

    assert task["status"] == "resolved"
    assert task["resolved_run"] == "2026-06-21:000002"


def test_housekeeper_task_can_be_deleted_and_returns_on_next_run(tmp_path) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    repository.create_member(first_name="Manual", last_name="Delete")
    housekeeper = Housekeeper(repository)
    findings = housekeeper.run(today=date(2026, 6, 21))
    finding = next(item for item in findings if item.code == "missing_birth_date")

    remaining = housekeeper.delete_task(finding.key)
    state_after_delete = json.loads((repository.root / "housekeeper.json").read_text(encoding="utf-8"))

    assert remaining == []
    assert state_after_delete["run_counter"] == 1
    assert state_after_delete["items"] == []

    recreated = housekeeper.run(today=date(2026, 6, 21))
    recreated_finding = next(item for item in recreated if item.code == "missing_birth_date")
    state_after_run = json.loads((repository.root / "housekeeper.json").read_text(encoding="utf-8"))
    recreated_task = next(item for item in state_after_run["items"] if item["key"] == recreated_finding.key)

    assert recreated_finding.key == finding.key
    assert recreated_task["first_seen_run"] == "2026-06-21:000002"


def test_housekeeper_removes_items_for_deleted_member_directory(tmp_path) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    member = repository.create_member(first_name="Deleted", last_name="Member")
    housekeeper = Housekeeper(repository)
    assert housekeeper.run(today=date(2026, 6, 21))
    shutil.rmtree(repository.members_root / member.member_id)

    findings = housekeeper.run(today=date(2026, 6, 21))
    state = json.loads((repository.root / "housekeeper.json").read_text(encoding="utf-8"))

    assert findings == []
    assert not any(item.get("member_id") == member.member_id for item in state["items"])


def test_housekeeper_run_can_target_a_single_member(tmp_path) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    member_a = repository.create_member(first_name="Alpha", last_name="Member")
    member_b = repository.create_member(first_name="Beta", last_name="Member")
    housekeeper = Housekeeper(repository)
    housekeeper.run(today=date(2026, 6, 21))
    state_before = json.loads((repository.root / "housekeeper.json").read_text(encoding="utf-8"))
    b_task_before = next(
        item for item in state_before["items"] if item.get("member_id") == member_b.member_id
    )

    member_a.birth_date = "1990-01-01"
    repository.save_member(member_a)
    findings = housekeeper.run(today=date(2026, 6, 21), member_id=member_a.member_id)

    a_findings = [item for item in findings if item.member_id == member_a.member_id]
    assert not any(item.code == "missing_birth_date" for item in a_findings)
    state_after = json.loads((repository.root / "housekeeper.json").read_text(encoding="utf-8"))
    b_task_after = next(
        item for item in state_after["items"] if item.get("member_id") == member_b.member_id
    )
    assert b_task_after == b_task_before


def test_housekeeper_run_rejects_unknown_member_id(tmp_path) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    housekeeper = Housekeeper(repository)

    with pytest.raises(RepositoryError, match="nicht gefunden"):
        housekeeper.run(today=date(2026, 6, 21), member_id="does-not-exist")


def test_housekeeper_run_retroactive_override_applies_only_to_that_run(tmp_path) -> None:
    repository = MemberRepository(tmp_path)
    repository.initialize()
    member = repository.create_member(first_name="Retro", last_name="Member", birth_date="1990-01-01")
    member.status = "active"
    member.accepted_at = "2024-04-15"
    member.membership_started_at = "2024-04-15"
    member.payment_frequency = "annual"
    repository.save_member(member)
    housekeeper = Housekeeper(repository)

    housekeeper.run(today=date(2026, 6, 21), member_id=member.member_id, retroactive_claims=True)

    claim_keys = {claim["claim_key"] for claim in repository.get_contributions(member.member_id).claims}
    assert "membership-fee:2024:annual" in claim_keys
    assert housekeeper.settings.retroactive_claims is False

    other = repository.create_member(first_name="NoRetro", last_name="Member", birth_date="1990-01-01")
    other.status = "active"
    other.accepted_at = "2024-04-15"
    other.membership_started_at = "2024-04-15"
    other.payment_frequency = "annual"
    repository.save_member(other)
    housekeeper.run(today=date(2026, 6, 21), member_id=other.member_id)

    other_claim_keys = {claim["claim_key"] for claim in repository.get_contributions(other.member_id).claims}
    assert "membership-fee:2024:annual" not in other_claim_keys
    assert "membership-fee:2026:annual" in other_claim_keys
