from __future__ import annotations

from collections.abc import Mapping
from dataclasses import replace
from datetime import datetime
from pathlib import Path

from blackout_notifier.clients import ExternalServiceError
from blackout_notifier.models import Outage
from blackout_notifier.service import OutageService
from blackout_notifier.state import StateRepository


class FakeSaapa:
    def __init__(self, snapshots: Mapping[str, list[Outage] | Exception]) -> None:
        self.snapshots = dict(snapshots)
        self.calls: list[tuple[str, str, str]] = []

    def fetch(self, bill_id: str, from_date: str, to_date: str) -> list[Outage]:
        self.calls.append((bill_id, from_date, to_date))
        result = self.snapshots[bill_id]
        if isinstance(result, Exception):
            raise result
        return result


class FakeNotifier:
    def __init__(self, *, fail: bool = False, fail_on_call: int | None = None) -> None:
        self.messages: list[tuple[str, str]] = []
        self.fail = fail
        self.fail_on_call = fail_on_call
        self.calls = 0

    def send(self, chat_id: str, text: str) -> None:
        self.calls += 1
        if self.fail or self.calls == self.fail_on_call:
            raise ExternalServiceError("Eitaa unavailable")
        self.messages.append((chat_id, text))


def _service(
    path: Path,
    saapa: FakeSaapa,
    notifier: FakeNotifier,
    bill_chat_ids: Mapping[str, tuple[str, ...]] | None = None,
) -> OutageService:
    return OutageService(
        bill_chat_ids=bill_chat_ids or {"123": ("chat-a",)},
        lookahead_days=5,
        saapa=saapa,  # type: ignore[arg-type]
        notifier=notifier,
        state_repository=StateRepository(path),
    )


def test_first_run_sends_upcoming_once(tmp_path: Path, now: datetime, outage: Outage) -> None:
    path = tmp_path / "state.json"
    saapa = FakeSaapa({"123": [outage]})
    notifier = FakeNotifier()
    service = _service(path, saapa, notifier)

    first = service.run(now)
    first_state = path.read_text(encoding="utf-8")
    second = service.run(now)

    assert first.sent == 1
    assert first.state_changed is True
    assert "خاموشی جدید" in notifier.messages[0][1]
    assert second.sent == 0
    assert second.state_changed is False
    assert path.read_text(encoding="utf-8") == first_state


def test_changed_outage_sends_field_diff(tmp_path: Path, now: datetime, outage: Outage) -> None:
    path = tmp_path / "state.json"
    notifier = FakeNotifier()
    saapa = FakeSaapa({"123": [outage]})
    service = _service(path, saapa, notifier)
    service.run(now)

    saapa.snapshots["123"] = [replace(outage, start_time="10:00")]
    result = service.run(now)

    assert result.sent == 1
    assert "تغییر در برنامه" in notifier.messages[-1][1]
    assert "09:00" in notifier.messages[-1][1]
    assert "10:00" in notifier.messages[-1][1]


def test_cancellation_requires_two_successful_omissions_and_can_reactivate(
    tmp_path: Path, now: datetime, outage: Outage
) -> None:
    path = tmp_path / "state.json"
    notifier = FakeNotifier()
    saapa = FakeSaapa({"123": [outage]})
    service = _service(path, saapa, notifier)
    service.run(now)
    notifier.messages.clear()

    saapa.snapshots["123"] = []
    first_missing = service.run(now)
    second_missing = service.run(now)

    assert first_missing.sent == 0
    assert second_missing.sent == 1
    assert "دو بررسی پیاپی" in notifier.messages[-1][1]

    saapa.snapshots["123"] = [outage]
    restored = service.run(now)
    assert restored.sent == 1
    assert "بازگشت برنامه" in notifier.messages[-1][1]


def test_failed_notification_does_not_advance_state(
    tmp_path: Path, now: datetime, outage: Outage
) -> None:
    path = tmp_path / "state.json"
    service = _service(path, FakeSaapa({"123": [outage]}), FakeNotifier(fail=True))

    result = service.run(now)

    assert result.sent == 0
    assert result.errors == ["Eitaa unavailable"]
    assert result.state_changed is False
    assert not path.exists()


def test_fetch_failure_for_one_bill_does_not_block_another(
    tmp_path: Path, now: datetime, outage: Outage
) -> None:
    saapa = FakeSaapa({"123": ExternalServiceError("SAAPA failed for 123"), "456": [outage]})
    notifier = FakeNotifier()
    service = _service(
        tmp_path / "state.json",
        saapa,
        notifier,
        {"123": ("chat-a",), "456": ("chat-b",)},
    )

    result = service.run(now)

    assert result.sent == 1
    assert result.errors == ["SAAPA failed for 123"]
    assert len(notifier.messages) == 1
    assert notifier.messages[0][0] == "chat-b"


def test_each_bill_is_sent_to_its_configured_chat(
    tmp_path: Path, now: datetime, outage: Outage
) -> None:
    second_outage = replace(outage, outage_number="404992999002")
    saapa = FakeSaapa({"123": [outage], "456": [second_outage]})
    notifier = FakeNotifier()
    service = _service(
        tmp_path / "state.json",
        saapa,
        notifier,
        {"123": ("chat-a",), "456": ("chat-b",)},
    )

    result = service.run(now)

    assert result.sent == 2
    assert [chat_id for chat_id, _ in notifier.messages] == ["chat-a", "chat-b"]
    assert "شناسه قبض: 123" in notifier.messages[0][1]
    assert "شناسه قبض: 456" in notifier.messages[1][1]


def test_one_bill_is_sent_to_all_its_configured_chats(
    tmp_path: Path, now: datetime, outage: Outage
) -> None:
    saapa = FakeSaapa({"123": [outage]})
    notifier = FakeNotifier()
    service = _service(
        tmp_path / "state.json",
        saapa,
        notifier,
        {"123": ("chat-a", "chat-b")},
    )

    result = service.run(now)

    assert result.sent == 2
    assert len(saapa.calls) == 1
    assert [chat_id for chat_id, _ in notifier.messages] == ["chat-a", "chat-b"]


def test_state_advances_only_after_all_chats_accept_notification(
    tmp_path: Path, now: datetime, outage: Outage
) -> None:
    path = tmp_path / "state.json"
    notifier = FakeNotifier(fail_on_call=2)
    service = _service(
        path,
        FakeSaapa({"123": [outage]}),
        notifier,
        {"123": ("chat-a", "chat-b", "chat-c")},
    )

    result = service.run(now)

    assert result.sent == 2
    assert result.errors == ["Eitaa unavailable"]
    assert [chat_id for chat_id, _ in notifier.messages] == ["chat-a", "chat-c"]
    assert result.state_changed is False
    assert not path.exists()


def test_partial_delivery_persists_only_successful_events(
    tmp_path: Path, now: datetime, outage: Outage
) -> None:
    second_outage = replace(outage, outage_number="404992999002", start_time="12:00")
    path = tmp_path / "state.json"
    notifier = FakeNotifier(fail_on_call=2)
    service = _service(path, FakeSaapa({"123": [outage, second_outage]}), notifier)

    result = service.run(now)
    records = StateRepository(path).load()["bills"]["123"]["events"]

    assert result.sent == 1
    assert result.errors == ["Eitaa unavailable"]
    assert set(records) == {outage.outage_number}


def test_failed_fetch_never_advances_cancellation_counter(
    tmp_path: Path, now: datetime, outage: Outage
) -> None:
    path = tmp_path / "state.json"
    saapa = FakeSaapa({"123": [outage]})
    service = _service(path, saapa, FakeNotifier())
    service.run(now)
    original_state = path.read_text(encoding="utf-8")

    saapa.snapshots["123"] = ExternalServiceError("SAAPA unavailable")
    result = service.run(now)

    assert result.errors == ["SAAPA unavailable"]
    assert result.state_changed is False
    assert path.read_text(encoding="utf-8") == original_state


def test_dry_run_never_writes_state(tmp_path: Path, now: datetime, outage: Outage) -> None:
    path = tmp_path / "state.json"
    service = _service(path, FakeSaapa({"123": [outage]}), FakeNotifier())

    result = service.run(now, persist=False)

    assert result.sent == 1
    assert result.state_changed is True
    assert not path.exists()
