import os
import stat
from datetime import date

import pytest

from ccma.domain.models import ContributionData
from ccma.services.documents import DocumentService
from ccma.services.housekeeper import Housekeeper
from ccma.services.reminder_mail import generate_and_send_reminder_mail
from ccma.storage.repository import MemberRepository, ReadOnlyStoreError

# Root ignores the permission bits this test relies on, so the read-only mount it
# stands in for cannot be simulated there.
pytestmark = pytest.mark.skipif(
    hasattr(os, "geteuid") and os.geteuid() == 0, reason="root bypasses file permissions"
)


def _set_writable(root, writable: bool) -> None:
    mode_add = stat.S_IWUSR
    for path in sorted(root.rglob("*"), reverse=True) + [root]:
        current = stat.S_IMODE(path.stat().st_mode)
        path.chmod(current | mode_add if writable else current & ~mode_add)


@pytest.fixture
def read_only_store(tmp_path):
    root = tmp_path / "store"
    repository = MemberRepository(root)
    repository.initialize()
    member = repository.create_member(
        first_name="Ada", last_name="Lovelace", birth_date="1990-01-01"
    )
    repository.save_contributions(
        member.member_id,
        ContributionData(
            claims=[
                {
                    "claim_id": "claim-1",
                    "claim_key": "overdue",
                    "title": "Offene Forderung",
                    "amount": "100.00",
                    "due_date": "2026-01-31",
                    "status": "open",
                }
            ]
        ),
    )
    _set_writable(root, False)
    yield MemberRepository(root), member
    _set_writable(root, True)


def test_a_writable_store_is_not_reported_as_read_only(tmp_path):
    repository = MemberRepository(tmp_path / "store")
    repository.initialize()

    assert repository.read_only is False


def test_store_directory_that_does_not_exist_yet_counts_as_writable(tmp_path):
    assert MemberRepository(tmp_path / "not" / "created" / "yet").read_only is False


def test_read_only_store_opens_and_stays_readable(read_only_store):
    repository, member = read_only_store

    assert repository.read_only is True
    repository.initialize()
    assert repository.validate() == []
    assert [item.member_id for item in repository.list_members()] == [member.member_id]
    assert repository.get_member(member.member_id).first_name == "Ada"
    assert repository.get_contributions(member.member_id).claims
    assert repository.get_events(member.member_id)
    assert repository.get_configuration()["organization"]["name"]


def test_every_write_is_refused_with_one_clear_message(read_only_store):
    repository, member = read_only_store
    stored = repository.get_member(member.member_id)

    for action in (
        lambda: repository.save_member(stored),
        lambda: repository.create_member(first_name="Grace", last_name="Hopper"),
        lambda: repository.create_manual_claim(
            member.member_id, title="Beitrag", amount="10.00", due_date="2026-09-30"
        ),
        lambda: repository.save_contributions(member.member_id, ContributionData()),
        lambda: repository.append_event(
            member.member_id, event_type="comment", summary="Test", actor_type="user"
        ),
        lambda: repository.save_organization({"name": "Test", "iban": "", "bic": ""}),
        lambda: repository.create_asset(label="Beamer"),
    ):
        with pytest.raises(ReadOnlyStoreError, match="schreibgeschützt"):
            action()


def test_housekeeper_refuses_to_run_instead_of_failing_on_its_lock(read_only_store):
    repository, _member = read_only_store

    with pytest.raises(ReadOnlyStoreError, match="schreibgeschützt"):
        Housekeeper(repository).run(today=date(2026, 2, 10))
    with pytest.raises(ReadOnlyStoreError, match="schreibgeschützt"):
        Housekeeper(repository).delete_task("some-key")
    assert not (repository.root / ".housekeeper.lock").exists()


def test_documents_and_mails_refuse_before_touching_the_member_file(read_only_store, tmp_path):
    repository, member = read_only_store

    service = DocumentService(repository)
    template = next(item for item in service.list_templates() if item.name == "Mitglied")
    with pytest.raises(ReadOnlyStoreError, match="schreibgeschützt"):
        service.generate(template, member.member_id, output_name="Test")
    with pytest.raises(ReadOnlyStoreError, match="schreibgeschützt"):
        generate_and_send_reminder_mail(
            repository,
            member.member_id,
            "claim-1",
            "reminder-1",
            delivery_mode="local",
            output_path=tmp_path / "Mahnung.eml",
            sender_name="Verwaltung",
            sender_email="verwaltung@example.org",
            signature="Der Vorstand",
        )


def test_uninitialized_read_only_store_reports_why_it_cannot_be_opened(tmp_path):
    root = tmp_path / "empty-store"
    root.mkdir()
    root.chmod(stat.S_IRUSR | stat.S_IXUSR)
    try:
        with pytest.raises(ReadOnlyStoreError, match="repository.json"):
            MemberRepository(root).initialize()
    finally:
        root.chmod(stat.S_IRWXU)


def test_remounting_writable_is_picked_up_without_a_restart(read_only_store):
    repository, _member = read_only_store
    assert repository.read_only is True

    _set_writable(repository.root, True)

    assert repository.read_only is True, "the cached answer must stay put until refreshed"
    assert repository.refresh_read_only() is False
    repository.append_event(
        _member.member_id,
        event_type="comment",
        summary="Wieder beschreibbar",
        actor_type="user",
    )


def test_startup_sequence_completes_without_the_housekeeper(read_only_store):
    """Mirrors what the splash screen's worker thread does -- this is the sequence
    that used to abort the whole start with a PermissionError."""
    repository, member = read_only_store

    repository.initialize()
    errors = repository.validate()
    members = repository.list_members()
    findings = [] if repository.read_only else Housekeeper(repository).run()

    assert errors == []
    assert [item.member_id for item in members] == [member.member_id]
    assert findings == []
    assert repository.member_count() == 1


def test_mail_templates_fall_back_to_the_shipped_defaults(read_only_store):
    """A store that predates mail templates has no templates/mail/ directory, and a
    read-only one cannot get it -- the shipped default has to carry the session."""
    from ccma.domain.mail_templates import default_mail_template

    repository, _member = read_only_store
    _set_writable(repository.root, True)
    for path in repository.mail_templates_root.glob("*.txt"):
        path.unlink()
    _set_writable(repository.root, False)

    assert repository.get_mail_template("welcome") == default_mail_template("welcome")
    assert not list(repository.mail_templates_root.glob("*.txt")), "nothing was written"
    with pytest.raises(ReadOnlyStoreError, match="schreibgeschützt"):
        repository.save_mail_template("welcome", subject="Moin", body="Kurz.")
