"""Unit tests for ``mgf.common.doctor`` — doctor-bundle primitive
(0.36.0)."""

from __future__ import annotations

import json
from datetime import UTC, datetime
from typing import TYPE_CHECKING

import pytest

if TYPE_CHECKING:
    from pathlib import Path

from mgf.common.doctor import DoctorBundle, ReportSection

# ---------------------------------------------------------------------------
# ReportSection — basic shape
# ---------------------------------------------------------------------------


class TestReportSection:
    def test_construction(self) -> None:
        s = ReportSection(name="foo", status="ok", details={"x": 1})
        assert s.name == "foo"
        assert s.status == "ok"
        assert s.details == {"x": 1}

    def test_frozen(self) -> None:
        from dataclasses import FrozenInstanceError

        s = ReportSection(name="foo", status="ok")
        with pytest.raises(FrozenInstanceError):
            s.name = "bar"  # type: ignore[misc]

    def test_with_details_merged(self) -> None:
        s = ReportSection(name="x", status="ok", details={"a": 1, "b": 2})
        s2 = s.with_details_merged({"b": 99, "c": 3})
        assert s2.details == {"a": 1, "b": 99, "c": 3}
        # Original unchanged (frozen).
        assert s.details == {"a": 1, "b": 2}


# ---------------------------------------------------------------------------
# DoctorBundle — composition + status aggregation
# ---------------------------------------------------------------------------


class TestBundleComposition:
    def test_empty_bundle(self) -> None:
        b = DoctorBundle(app_name="x", app_version="0.0.1")
        assert b.sections() == ()
        assert b.overall_status == "ok"

    def test_add_section_appends(self) -> None:
        b = DoctorBundle(app_name="x", app_version="0.0.1")
        b.add_section(ReportSection(name="a", status="ok"))
        b.add_section(ReportSection(name="b", status="ok"))
        assert [s.name for s in b.sections()] == ["a", "b"]

    def test_add_section_idempotent_by_name(self) -> None:
        b = DoctorBundle(app_name="x", app_version="0.0.1")
        b.add_section(ReportSection(name="a", status="ok"))
        b.add_section(ReportSection(name="a", status="warn", details={"new": True}))
        # Replaced, not duplicated.
        assert len(b.sections()) == 1
        assert b.sections()[0].status == "warn"
        assert b.sections()[0].details == {"new": True}

    def test_overall_status_ok(self) -> None:
        b = DoctorBundle(app_name="x", app_version="0.0.1")
        b.add_section(ReportSection(name="a", status="ok"))
        b.add_section(ReportSection(name="b", status="ok"))
        assert b.overall_status == "ok"

    def test_overall_status_warn_dominates_ok(self) -> None:
        b = DoctorBundle(app_name="x", app_version="0.0.1")
        b.add_section(ReportSection(name="a", status="ok"))
        b.add_section(ReportSection(name="b", status="warn"))
        b.add_section(ReportSection(name="c", status="ok"))
        assert b.overall_status == "warn"

    def test_overall_status_fail_dominates(self) -> None:
        b = DoctorBundle(app_name="x", app_version="0.0.1")
        b.add_section(ReportSection(name="a", status="ok"))
        b.add_section(ReportSection(name="b", status="warn"))
        b.add_section(ReportSection(name="c", status="fail"))
        assert b.overall_status == "fail"


# ---------------------------------------------------------------------------
# Rendering — text + JSON
# ---------------------------------------------------------------------------


class TestRendering:
    def test_text_includes_basics(self) -> None:
        b = DoctorBundle(
            app_name="my-app",
            app_version="1.0.0",
            when=datetime(2026, 5, 18, 14, 30, tzinfo=UTC),
        )
        b.add_section(ReportSection(name="settings", status="ok", details={"key": "value"}))

        out = b.render_text()
        assert "=== my-app doctor report ===" in out
        assert "app_version: 1.0.0" in out
        assert "2026-05-18T14:30:00+00:00" in out
        assert "settings" in out
        assert "key: value" in out
        assert "overall:" in out

    def test_text_empty(self) -> None:
        b = DoctorBundle(app_name="x", app_version="0.0.1")
        out = b.render_text()
        assert "no sections registered" in out

    def test_json_schema(self) -> None:
        b = DoctorBundle(app_name="my-app", app_version="1.0.0")
        b.add_section(
            ReportSection(
                name="settings",
                status="ok",
                details={"app_name": "my-app"},
            )
        )
        payload = b.render_json()
        assert payload["schema_version"] == 1
        assert payload["app_name"] == "my-app"
        assert payload["app_version"] == "1.0.0"
        assert payload["overall_status"] == "ok"
        assert len(payload["sections"]) == 1
        assert payload["sections"][0]["name"] == "settings"
        assert payload["sections"][0]["details"] == {"app_name": "my-app"}

    def test_json_is_json_serialisable(self) -> None:
        b = DoctorBundle(app_name="x", app_version="0.0.1")
        b.add_section(ReportSection(name="a", status="fail", details={"err": "boom"}))
        # Must not raise.
        serialised = json.dumps(b.render_json())
        assert "fail" in serialised
        assert "boom" in serialised


# ---------------------------------------------------------------------------
# Disk output — 0o600 + atomic
# ---------------------------------------------------------------------------


class TestWriteToDisk:
    def test_writes_text_with_0o600(self, tmp_path: Path) -> None:
        b = DoctorBundle(app_name="x", app_version="0.0.1")
        b.add_section(ReportSection(name="a", status="ok"))

        target = tmp_path / "report.txt"
        b.write_to(target)

        assert target.exists()
        assert (target.stat().st_mode & 0o777) == 0o600
        content = target.read_text()
        assert "=== x doctor report ===" in content

    def test_writes_json_with_0o600(self, tmp_path: Path) -> None:
        b = DoctorBundle(app_name="x", app_version="0.0.1")
        b.add_section(ReportSection(name="a", status="ok"))

        target = tmp_path / "report.json"
        b.write_to(target, output_format="json")

        assert target.exists()
        assert (target.stat().st_mode & 0o777) == 0o600
        payload = json.loads(target.read_text())
        assert payload["app_name"] == "x"

    def test_unknown_format_rejected(self, tmp_path: Path) -> None:
        b = DoctorBundle(app_name="x", app_version="0.0.1")
        with pytest.raises(ValueError, match=r"text.*json"):
            b.write_to(tmp_path / "out", output_format="yaml")  # type: ignore[arg-type]

    def test_missing_parent_rejected(self, tmp_path: Path) -> None:
        b = DoctorBundle(app_name="x", app_version="0.0.1")
        target = tmp_path / "no-such-dir" / "report.txt"
        with pytest.raises(FileNotFoundError, match="does not exist"):
            b.write_to(target)

    def test_atomic_overwrite(self, tmp_path: Path) -> None:
        target = tmp_path / "report.txt"
        target.write_text("original-content")

        b = DoctorBundle(app_name="x", app_version="0.0.1")
        b.add_section(ReportSection(name="a", status="ok"))
        b.write_to(target)

        # Original content replaced.
        assert "original-content" not in target.read_text()
        # New content present.
        assert "=== x doctor report ===" in target.read_text()
        # Mode still 0o600.
        assert (target.stat().st_mode & 0o777) == 0o600
