"""Tests for HeartbeatMonitor critical upload behavior."""

from __future__ import annotations

import time
from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import Any

import pytest
from azure.core.exceptions import ServiceRequestError

from infra_core.azure.monitoring import HeartbeatMonitor


class UploadOnlyHeartbeatClient:
    """Minimal upload-only heartbeat client fake."""

    _mode = "strict"

    def __init__(
        self,
        *,
        configured: bool = True,
        effects: Sequence[Exception | None] | None = None,
    ) -> None:
        self._configured = configured
        self.upload_calls: list[tuple[Path, bool]] = []
        self._effects = list(effects or [])

    def is_configured(self) -> bool:
        return self._configured

    def upload_file(
        self,
        path: Path,
        *,
        blob_path: str | None = None,
        raise_on_error: bool = False,
    ) -> None:
        self.upload_calls.append((Path(path), raise_on_error))
        if self._effects:
            effect = self._effects.pop(0)
            if effect is not None:
                raise effect


class JsonHeartbeatClient:
    """Heartbeat client fake that exposes ``write_json``."""

    _mode = "strict"

    def __init__(
        self,
        *,
        configured: bool = True,
        effects: Sequence[Exception | None] | None = None,
    ) -> None:
        self._configured = configured
        self.write_calls: list[tuple[Path, dict[str, Any], bool]] = []
        self._effects = list(effects or [])

    def is_configured(self) -> bool:
        return self._configured

    def write_json(
        self,
        path: Path,
        data: Mapping[str, Any],
        *,
        raise_on_error: bool = False,
    ) -> Path:
        self.write_calls.append((Path(path), dict(data), raise_on_error))
        if self._effects:
            effect = self._effects.pop(0)
            if effect is not None:
                raise effect
        return Path(path)


class LegacyUploadOnlyHeartbeatClient:
    """Upload-only client that predates the ``raise_on_error`` keyword."""

    _mode = "strict"

    def __init__(self) -> None:
        self.uploaded: list[Path] = []

    def is_configured(self) -> bool:
        return True

    def upload_file(self, path: Path) -> None:
        self.uploaded.append(Path(path))


class LegacyWriteJsonHeartbeatClient:
    """Legacy ``write_json`` client without the ``raise_on_error`` keyword."""

    _mode = "strict"

    def __init__(self) -> None:
        self.writes: list[tuple[Path, dict[str, Any]]] = []

    def is_configured(self) -> bool:
        return True

    def write_json(self, path: Path, data: Mapping[str, Any]) -> Path:
        self.writes.append((Path(path), dict(data)))
        return Path(path)


class RecordingLogWriter:
    """Simple event recorder for heartbeat log writes."""

    def __init__(self) -> None:
        self.entries: list[tuple[str, dict[str, Any]]] = []

    def write(self, event: str, **properties: Any) -> dict[str, Any]:
        record = {"event": event, **properties}
        self.entries.append((event, record))
        return record


class TestHeartbeatCriticalUpload:
    """Test that heartbeat upload failures propagate (critical)."""

    def test_heartbeat_upload_success(self, tmp_path: Path) -> None:
        storage_root = tmp_path / "storage"
        client = UploadOnlyHeartbeatClient()

        monitor = HeartbeatMonitor(
            storage_root,
            interval_seconds=1.0,
            azure_client=client,
        )

        monitor.set_slug("test-slug")

        assert len(client.upload_calls) == 1
        called_path, raise_on_error = client.upload_calls[0]
        assert "heartbeat.json" in str(called_path)
        assert raise_on_error is True

    def test_heartbeat_upload_failure_propagates(self, tmp_path: Path) -> None:
        storage_root = tmp_path / "storage"
        client = UploadOnlyHeartbeatClient(effects=[ServiceRequestError("Connection timeout")])

        monitor = HeartbeatMonitor(
            storage_root,
            interval_seconds=1.0,
            azure_client=client,
        )

        with pytest.raises(ServiceRequestError, match="Connection timeout"):
            monitor.set_slug("test-slug")

    def test_heartbeat_write_json_failure_propagates(self, tmp_path: Path) -> None:
        storage_root = tmp_path / "storage"
        client = JsonHeartbeatClient(effects=[ServiceRequestError("Write failed")])

        monitor = HeartbeatMonitor(
            storage_root,
            interval_seconds=1.0,
            azure_client=client,
        )

        with pytest.raises(ServiceRequestError, match="Write failed"):
            monitor.set_slug("test-slug")

    def test_heartbeat_upload_type_error_is_not_retried(self, tmp_path: Path) -> None:
        storage_root = tmp_path / "storage"
        client = UploadOnlyHeartbeatClient(effects=[TypeError("Upload failed")])

        monitor = HeartbeatMonitor(
            storage_root,
            interval_seconds=1.0,
            azure_client=client,
        )

        with pytest.raises(TypeError, match="Upload failed"):
            monitor.set_slug("test-slug")

        assert len(client.upload_calls) == 1

    def test_heartbeat_write_json_type_error_is_not_retried(self, tmp_path: Path) -> None:
        storage_root = tmp_path / "storage"
        client = JsonHeartbeatClient(effects=[TypeError("Write failed")])

        monitor = HeartbeatMonitor(
            storage_root,
            interval_seconds=1.0,
            azure_client=client,
        )

        with pytest.raises(TypeError, match="Write failed"):
            monitor.set_slug("test-slug")

        assert len(client.write_calls) == 1


def test_heartbeat_unconfigured_client_skips_upload(tmp_path: Path) -> None:
    storage_root = tmp_path / "storage"
    client = UploadOnlyHeartbeatClient(configured=False)

    monitor = HeartbeatMonitor(
        storage_root,
        interval_seconds=1.0,
        azure_client=client,
    )

    with pytest.raises(RuntimeError, match="not configured|mode"):
        monitor.set_slug("test-slug")

    assert not client.upload_calls


def test_heartbeat_legacy_upload_without_raise_on_error_kwarg(tmp_path: Path) -> None:
    storage_root = tmp_path / "storage"
    client = LegacyUploadOnlyHeartbeatClient()

    monitor = HeartbeatMonitor(
        storage_root,
        interval_seconds=1.0,
        azure_client=client,
    )

    monitor.set_slug("test-slug")

    assert client.uploaded


def test_heartbeat_legacy_write_json_without_raise_on_error_kwarg(tmp_path: Path) -> None:
    storage_root = tmp_path / "storage"
    client = LegacyWriteJsonHeartbeatClient()

    monitor = HeartbeatMonitor(
        storage_root,
        interval_seconds=1.0,
        azure_client=client,
    )

    monitor.set_slug("test-slug")

    assert client.writes


def test_heartbeat_pulse_upload_failure_propagates(tmp_path: Path) -> None:
    storage_root = tmp_path / "storage"
    client = UploadOnlyHeartbeatClient(effects=[None, ServiceRequestError("Upload failed")])

    monitor = HeartbeatMonitor(
        storage_root,
        interval_seconds=0.05,
        azure_client=client,
    )

    monitor.set_slug("test-slug")
    time.sleep(0.06)

    with pytest.raises(ServiceRequestError, match="Upload failed"):
        monitor.pulse()


def test_heartbeat_stop_upload_failure_propagates(tmp_path: Path) -> None:
    storage_root = tmp_path / "storage"
    client = UploadOnlyHeartbeatClient(effects=[None, ServiceRequestError("Upload failed")])

    monitor = HeartbeatMonitor(
        storage_root,
        interval_seconds=1.0,
        azure_client=client,
    )

    monitor.set_slug("test-slug")

    with pytest.raises(ServiceRequestError, match="Upload failed"):
        monitor.stop()


class TestHeartbeatBackwardCompatibility:
    """Test that heartbeat changes don't break existing functionality."""

    def test_heartbeat_with_log_writer(self, tmp_path: Path) -> None:
        storage_root = tmp_path / "storage"
        client = UploadOnlyHeartbeatClient()
        log_writer = RecordingLogWriter()

        monitor = HeartbeatMonitor(
            storage_root,
            interval_seconds=1.0,
            azure_client=client,
            log_writer=log_writer,
        )

        monitor.set_slug("test-slug")

        assert len(log_writer.entries) == 1
        event, record = log_writer.entries[0]
        assert event == "heartbeat"
        assert record["slug"] == "test-slug"

    def test_background_failure_interrupts_main(
        self,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        storage_root = tmp_path / "storage"
        client = UploadOnlyHeartbeatClient(effects=[ServiceRequestError("Upload failed")])
        interrupted: list[bool] = []

        monkeypatch.setattr(
            "infra_core.azure.monitoring._thread.interrupt_main",
            lambda: interrupted.append(True),
            raising=False,
        )

        monitor = HeartbeatMonitor(
            storage_root,
            interval_seconds=0.05,
            azure_client=client,
            interrupt_main_on_error=True,
        )

        monitor.start()
        try:
            deadline = time.monotonic() + 1.0
            while not client.upload_calls and time.monotonic() < deadline:
                time.sleep(0.01)

            assert client.upload_calls, "Background upload never started"

            with pytest.raises(ServiceRequestError, match="Upload failed"):
                monitor.stop()
        finally:
            monitor._stop.set()

        deadline = time.monotonic() + 0.5
        while not interrupted and time.monotonic() < deadline:
            time.sleep(0.01)

        assert interrupted, "Main thread should be interrupted on background failure"
