"""NCA-002 tests for `meshnet-node doctor`.

The unit tests inject a fake backend, so none of them download a model, import
Torch, or need a GPU. The one test that runs a real model is `integration`-marked
and takes its model identity from the environment — it has no model default, on
purpose: the doctor is model-agnostic and so is its test.
"""

import base64
import json
import os
import struct
from pathlib import Path

import pytest

from meshnet_node import doctor
from meshnet_node.capability import STATUS_FAILED, STATUS_PASSED, CapabilityReport
from meshnet_node.doctor import (
    CATEGORY_FORWARD_FAILED,
    CATEGORY_INSUFFICIENT_MEMORY,
    CATEGORY_INVALID_SHARD,
    CATEGORY_MISSING_DEPENDENCY,
    CATEGORY_NO_MODEL,
    CATEGORY_UNSUPPORTED_RECIPE,
    PROBE_TOKENS,
    DoctorError,
    DoctorSelection,
    build_probe_input,
    classify_failure,
    probe_forward,
    render_result,
    resolve_selection,
    run_doctor,
    select_recipes,
    write_reports,
)
from meshnet_node.model_backend import (
    InsufficientVRAMError,
    MissingModelDependencyError,
    UnsupportedRecipeParam,
    validate_recipe_params,
)
from meshnet_node.recipe_manifest import parse_recipe_manifest

# Deliberately not a model this project ships against: nothing here may special-case it.
FIXTURE_MODEL = "acme-labs/Widget-9000-Instruct"

MANIFEST = parse_recipe_manifest(
    {
        "schema_version": 1,
        "catalogue_version": "test-1",
        "recipes": [
            {"id": "baseline", "version": "1", "backend_id": "torch-transformers"},
            {
                "id": "stateless",
                "version": "2",
                "backend_id": "torch-transformers",
                "params": {"use_cache": False},
            },
        ],
    },
    source="<test manifest>",
)


class _Payload:
    """Stands in for model_backend.TensorPayload."""

    def __init__(self, body: bytes, shape: list[int]) -> None:
        self.body = body
        self.shape = shape
        self.attention_mask_header = None
        self.position_ids_header = None


class _TailToken:
    """Stands in for model_backend.TailTokenResult."""

    def __init__(self, token_id: int = 7) -> None:
        self.token_id = token_id
        self.text = "ok"


class _Device:
    def __init__(self, type_: str = "cpu") -> None:
        self.type = type_


class _FakeBackend:
    """A backend that loads but records exactly how it was driven."""

    hidden_size = 8

    def __init__(
        self,
        *,
        is_head: bool = True,
        is_tail: bool = False,
        shard_start: int = 0,
        shard_end: int = 3,
        forward_error: Exception | None = None,
    ) -> None:
        self.model_id = FIXTURE_MODEL
        self.is_head = is_head
        self.is_tail = is_tail
        self.shard_start = shard_start
        self.shard_end = shard_end
        self.device = _Device("cpu")
        self.forward_error = forward_error
        self.encoded_prompts: list[str] = []
        self.forwards: list[dict] = []

    def encode_prompt(self, prompt: str):
        if self.forward_error is not None:
            raise self.forward_error
        self.encoded_prompts.append(prompt)
        return _Payload(b"\x00" * (PROBE_TOKENS * self.hidden_size * 2),
                        [1, PROBE_TOKENS, self.hidden_size])

    def forward_bytes(
        self,
        body,
        shape,
        attention_mask_header,
        position_ids_header,
        start_layer=None,
        **kwargs,
    ):
        if self.forward_error is not None:
            raise self.forward_error
        self.forwards.append(
            {
                "body_len": len(body),
                "shape": shape,
                "start_layer": start_layer,
                "attention_mask_header": attention_mask_header,
                "position_ids_header": position_ids_header,
            }
        )
        if self.is_tail:
            return _TailToken()
        return _Payload(body, shape)


def _loader(backend=None, *, error: Exception | None = None):
    """A load_backend stub that records the (selection, recipe) pairs it saw."""
    calls: list[tuple[DoctorSelection, object]] = []

    def load(selection, recipe):
        calls.append((selection, recipe))
        if error is not None:
            raise error
        return backend if backend is not None else _FakeBackend()

    load.calls = calls  # type: ignore[attr-defined]
    return load


def _selection(**overrides) -> DoctorSelection:
    kwargs = dict(model_id=FIXTURE_MODEL, shard_start=0, shard_end=3)
    kwargs.update(overrides)
    return DoctorSelection(**kwargs)


# --- selection resolves the same as startup ---------------------------------


def test_resolve_selection_uses_the_configured_repo_shard_and_quantization():
    selection = resolve_selection(
        {
            "model_hf_repo": FIXTURE_MODEL,
            "model_name": "Widget-9000-Instruct",
            "shard_start": 4,
            "shard_end": 11,
            "quantization": "bf16",  # startup normalizes this to bfloat16
            "download_dir": "/models",
        }
    )

    assert selection.model_id == FIXTURE_MODEL
    assert (selection.shard_start, selection.shard_end) == (4, 11)
    assert selection.quantization == "bfloat16"
    assert selection.cache_dir == Path("/models")


def test_resolve_selection_defaults_to_the_whole_model_like_startup():
    """With no pinned shard, startup serves layers 0..n-1 — so doctor validates that."""
    seen: list[tuple[str, Path | None]] = []

    def detect(model_id, cache_dir):
        seen.append((model_id, cache_dir))
        return 24

    selection = resolve_selection(
        {"model_hf_repo": FIXTURE_MODEL}, detect_layers=detect
    )

    assert (selection.shard_start, selection.shard_end) == (0, 23)
    assert seen == [(FIXTURE_MODEL, None)]


def test_resolve_selection_without_a_model_is_actionable():
    with pytest.raises(DoctorError) as exc:
        resolve_selection({"model_hf_repo": "", "model_name": ""})

    assert exc.value.category == CATEGORY_NO_MODEL
    assert "--model" in exc.value.hint


def test_resolve_selection_rejects_an_inverted_shard_range():
    with pytest.raises(DoctorError) as exc:
        resolve_selection(
            {"model_hf_repo": FIXTURE_MODEL, "shard_start": 9, "shard_end": 2}
        )

    assert exc.value.category == CATEGORY_INVALID_SHARD


def test_resolve_selection_reports_an_unreadable_model_config():
    with pytest.raises(DoctorError) as exc:
        resolve_selection(
            {"model_hf_repo": FIXTURE_MODEL}, detect_layers=lambda *_: None
        )

    assert exc.value.category == doctor.CATEGORY_MODEL_UNAVAILABLE
    assert "--shard-start" in str(exc.value)


# --- the bounded real forward ------------------------------------------------


def test_a_pass_requires_a_real_forward_through_the_selected_shard():
    """Hardware being fine is not the bar: the shard itself has to execute."""
    backend = _FakeBackend(is_head=True)

    result = run_doctor(
        _selection(), manifest=MANIFEST, load_backend=_loader(backend), now=lambda: 1000.0
    )

    assert result.passed
    assert backend.encoded_prompts == [doctor.PROBE_PROMPT]
    report = result.reports[0]
    assert report.status == STATUS_PASSED
    assert report.model.model_id == FIXTURE_MODEL
    assert (report.shard.start, report.shard.end) == (0, 3)


def test_a_backend_that_loads_but_cannot_forward_never_passes():
    """The regression this story exists for: a load is not a validation."""
    backend = _FakeBackend(forward_error=RuntimeError("kernel exploded"))

    result = run_doctor(
        _selection(), manifest=MANIFEST, load_backend=_loader(backend), now=lambda: 1.0
    )

    assert not result.passed
    assert result.exit_code == 1
    report = result.reports[0]
    assert report.status == STATUS_FAILED
    assert result.results[0].category == CATEGORY_FORWARD_FAILED
    assert any("kernel exploded" in d for d in report.diagnostics)


def test_a_mid_shard_is_probed_with_peer_shaped_hidden_states():
    backend = _FakeBackend(is_head=False, shard_start=4, shard_end=7)

    detail = probe_forward(backend)

    assert detail["probe"] == "hidden-states"
    assert backend.encoded_prompts == []
    forward = backend.forwards[0]
    assert forward["shape"] == [1, PROBE_TOKENS, backend.hidden_size]
    # bfloat16 == 2 bytes per element, and the probe stays bounded to PROBE_TOKENS.
    assert forward["body_len"] == PROBE_TOKENS * backend.hidden_size * 2
    assert forward["start_layer"] == 4


def test_a_head_and_tail_shard_also_decodes_so_the_lm_head_is_covered():
    backend = _FakeBackend(is_head=True, is_tail=True, shard_end=5)

    detail = probe_forward(backend)

    assert detail["probe"] == "prompt+decode"
    assert detail["output"] == "token"
    # Re-entering above the last layer decodes without re-running any layer.
    assert backend.forwards[0]["start_layer"] == 6


def test_a_tail_shard_that_decodes_a_token_passes():
    backend = _FakeBackend(is_head=False, is_tail=True, shard_start=8, shard_end=11)

    detail = probe_forward(backend)

    assert detail == {
        "probe": "hidden-states",
        "tokens": PROBE_TOKENS,
        "output": "token",
        "token_id": 7,
    }


def test_an_empty_forward_result_is_a_failure_not_a_pass():
    backend = _FakeBackend(is_head=False)
    backend.forward_bytes = lambda *a, **k: _Payload(b"", [])  # type: ignore[assignment]

    with pytest.raises(DoctorError) as exc:
        probe_forward(backend)

    assert exc.value.category == CATEGORY_FORWARD_FAILED


def test_a_backend_with_no_hidden_size_cannot_be_probed():
    with pytest.raises(DoctorError) as exc:
        build_probe_input(0)

    assert exc.value.category == CATEGORY_FORWARD_FAILED


def test_probe_headers_decode_as_int64_tensors():
    probe = build_probe_input(hidden_size=8, tokens=3)

    shape, encoded = probe.position_ids_header.split(":", 1)
    raw = base64.b64decode(encoded)
    assert shape == "1,3"
    assert list(struct.unpack("<3q", raw)) == [0, 1, 2]


# --- recipes -----------------------------------------------------------------


def test_the_default_run_validates_only_the_selected_recipe():
    """Onboarding must not pay to validate recipes the node was not asked to serve."""
    load = _loader()

    result = run_doctor(_selection(), manifest=MANIFEST, load_backend=load)

    assert [r.recipe.id for r in result.results] == ["baseline"]
    assert len(load.calls) == 1


def test_all_recipes_is_explicit_and_validates_every_recipe():
    load = _loader()

    result = run_doctor(
        _selection(), manifest=MANIFEST, load_backend=load, all_recipes=True
    )

    assert [r.recipe.id for r in result.results] == ["baseline", "stateless"]
    assert len(load.calls) == 2
    assert result.passed


def test_each_recipe_reaches_the_backend_that_runs_it():
    """A recipe that never reaches the loader was not really validated."""
    load = _loader()

    run_doctor(_selection(), manifest=MANIFEST, load_backend=load, all_recipes=True)

    params = [recipe.params for _, recipe in load.calls]
    assert params == [{}, {"use_cache": False}]


def test_an_unknown_recipe_names_the_ones_that_exist():
    with pytest.raises(DoctorError) as exc:
        select_recipes(MANIFEST, recipe_id="does-not-exist")

    assert exc.value.category == CATEGORY_UNSUPPORTED_RECIPE
    assert "baseline" in str(exc.value)


def test_recipe_and_all_recipes_are_mutually_exclusive():
    with pytest.raises(DoctorError):
        select_recipes(MANIFEST, recipe_id="baseline", all_recipes=True)


def test_a_recipe_the_backend_cannot_apply_is_a_failure_not_a_silent_pass():
    validate_recipe_params({"use_cache": False, "attn_implementation": "eager"})

    with pytest.raises(UnsupportedRecipeParam) as exc:
        validate_recipe_params({"sparkle_mode": True})

    assert "sparkle_mode" in str(exc.value)
    assert classify_failure(exc.value) == CATEGORY_UNSUPPORTED_RECIPE


def test_the_shipped_recipes_are_all_applicable_by_the_backend():
    """recipes.json and the backend's supported params must not drift apart."""
    from meshnet_node.recipe_manifest import load_recipe_manifest

    for recipe in load_recipe_manifest().recipes:
        validate_recipe_params(recipe.params)


# --- failure reporting -------------------------------------------------------


@pytest.mark.parametrize(
    "exc, category",
    [
        (MissingModelDependencyError("no torch"), CATEGORY_MISSING_DEPENDENCY),
        (InsufficientVRAMError("too big"), CATEGORY_INSUFFICIENT_MEMORY),
        (UnsupportedRecipeParam("nope"), CATEGORY_UNSUPPORTED_RECIPE),
        (ValueError("shard_end 99 exceeds last layer index 23"), CATEGORY_INVALID_SHARD),
        (FileNotFoundError("config.json"), doctor.CATEGORY_MODEL_UNAVAILABLE),
        (RuntimeError("something else"), doctor.CATEGORY_LOAD_FAILED),
    ],
)
def test_load_failures_are_classified_into_actionable_categories(exc, category):
    result = run_doctor(
        _selection(), manifest=MANIFEST, load_backend=_loader(error=exc)
    )

    assert not result.passed
    item = result.results[0]
    assert item.category == category
    assert item.hint  # every category tells the operator what to do next
    assert item.report.status == STATUS_FAILED


def test_a_failure_report_carries_the_hint_and_no_traceback():
    result = run_doctor(
        _selection(),
        manifest=MANIFEST,
        load_backend=_loader(error=InsufficientVRAMError("insufficient VRAM to load")),
    )

    diagnostics = " ".join(result.reports[0].diagnostics)
    assert "insufficient VRAM to load" in diagnostics
    assert "--shard-start" in diagnostics  # the actionable next step
    assert "Traceback" not in diagnostics
    assert ".py" not in diagnostics  # no file/line noise from a stack


def test_a_failure_report_still_identifies_what_was_being_validated():
    """NCA-003 refuses to register without a matching report — including a failed one."""
    result = run_doctor(
        _selection(shard_start=4, shard_end=9, quantization="int8"),
        manifest=MANIFEST,
        load_backend=_loader(error=RuntimeError("boom")),
        now=lambda: 4242.0,
    )

    report = result.reports[0]
    assert report.identity_key() == (
        FIXTURE_MODEL, 4, 9, "baseline", "1", "torch-transformers", "unknown",
    )
    assert report.validated_at == 4242.0
    assert report.recipe.catalogue_version == "test-1"


def test_the_report_records_the_device_the_forward_actually_ran_on():
    result = run_doctor(
        _selection(), manifest=MANIFEST, load_backend=_loader(_FakeBackend())
    )

    assert result.reports[0].backend.device == "cpu"
    assert result.reports[0].backend.backend_id == "torch-transformers"


def test_reports_round_trip_through_the_written_json(tmp_path):
    result = run_doctor(
        _selection(), manifest=MANIFEST, load_backend=_loader(), all_recipes=True
    )

    path = write_reports(result.reports, tmp_path / "nested" / "capability.json")
    payload = json.loads(path.read_text())

    assert [CapabilityReport.from_dict(d).recipe.recipe_id for d in payload] == [
        "baseline",
        "stateless",
    ]


def test_a_single_report_is_written_as_one_object():
    """One selected recipe writes one report — the shape NCA-003 will read."""
    import tempfile

    result = run_doctor(_selection(), manifest=MANIFEST, load_backend=_loader())
    with tempfile.TemporaryDirectory() as tmp:
        path = write_reports(result.reports, Path(tmp) / "capability.json")
        report = CapabilityReport.from_json(path.read_text())

    assert report.passed


def test_the_summary_tells_a_failing_operator_what_to_fix():
    result = run_doctor(
        _selection(),
        manifest=MANIFEST,
        load_backend=_loader(error=MissingModelDependencyError("torch is not installed")),
    )

    text = render_result(result, report_path=Path("/tmp/capability.json"))

    assert "FAIL" in text
    assert CATEGORY_MISSING_DEPENDENCY in text
    assert "torch is not installed" in text
    assert "/tmp/capability.json" in text
    assert "Traceback" not in text


def test_the_summary_names_the_shard_that_passed():
    result = run_doctor(_selection(), manifest=MANIFEST, load_backend=_loader())

    text = render_result(result)

    assert "PASS" in text
    assert FIXTURE_MODEL in text
    assert "layers 0–3" in text


# --- the CLI wiring ----------------------------------------------------------


def _run_cli(monkeypatch, argv, backend=None, error=None):
    """Drive `meshnet-node doctor` end to end with an injected backend."""
    import sys

    from meshnet_node import cli, config

    monkeypatch.setattr(
        config, "load_config", lambda *a, **k: {
            "model_hf_repo": FIXTURE_MODEL,
            "shard_start": 0,
            "shard_end": 3,
            "quantization": "auto",
        }
    )
    monkeypatch.setattr(
        doctor, "default_load_backend", _loader(backend, error=error)
    )
    monkeypatch.setattr(doctor, "load_recipe_manifest", lambda *a, **k: MANIFEST)
    monkeypatch.setattr(sys, "argv", ["meshnet-node", *argv])

    with pytest.raises(SystemExit) as exit_info:
        cli.main()
    return exit_info.value.code


def test_cli_doctor_exits_zero_and_writes_a_passing_report(monkeypatch, capsys, tmp_path):
    report = tmp_path / "capability.json"

    code = _run_cli(monkeypatch, ["doctor", "--report", str(report)], backend=_FakeBackend())

    assert code == 0
    assert capsys.readouterr().out.count("PASS") == 1
    assert CapabilityReport.from_json(report.read_text()).passed


def test_cli_doctor_exits_non_zero_and_writes_the_failed_report(monkeypatch, capsys, tmp_path):
    report = tmp_path / "capability.json"

    code = _run_cli(
        monkeypatch,
        ["doctor", "--report", str(report)],
        error=InsufficientVRAMError("insufficient VRAM to load 24 layers"),
    )

    out = capsys.readouterr().out
    assert code == 1
    assert "FAIL" in out
    assert CATEGORY_INSUFFICIENT_MEMORY in out
    assert "Traceback" not in out  # no raw traceback by default
    assert CapabilityReport.from_json(report.read_text()).status == STATUS_FAILED


def test_cli_doctor_all_recipes_is_opt_in(monkeypatch, capsys, tmp_path):
    report = tmp_path / "capability.json"

    code = _run_cli(
        monkeypatch,
        ["doctor", "--all-recipes", "--report", str(report)],
        backend=_FakeBackend(),
    )

    assert code == 0
    assert capsys.readouterr().out.count("PASS") == 2
    assert len(json.loads(report.read_text())) == 2


def test_cli_doctor_json_prints_the_capability_report(monkeypatch, capsys, tmp_path):
    code = _run_cli(
        monkeypatch,
        ["doctor", "--json", "--report", str(tmp_path / "c.json")],
        backend=_FakeBackend(),
    )

    payload = json.loads(capsys.readouterr().out)
    assert code == 0
    assert payload[0]["model"]["model_id"] == FIXTURE_MODEL


def test_cli_doctor_flags_select_what_is_validated(monkeypatch, capsys, tmp_path):
    """`doctor --shard-start/--shard-end` validates the shard startup would load."""
    report = tmp_path / "capability.json"

    code = _run_cli(
        monkeypatch,
        ["doctor", "--shard-start", "2", "--shard-end", "5", "--report", str(report)],
        backend=_FakeBackend(),
    )

    written = CapabilityReport.from_json(report.read_text())
    assert code == 0
    assert (written.shard.start, written.shard.end) == (2, 5)


# --- the real-model smoke test ----------------------------------------------

# Model identity comes from the environment; there is no default, so this test
# never smuggles a vendor-specific assumption into the suite.
DOCTOR_MODEL = os.environ.get("MESHNET_DOCTOR_MODEL")
DOCTOR_SHARD_START = int(os.environ.get("MESHNET_DOCTOR_SHARD_START", "0"))
DOCTOR_SHARD_END = os.environ.get("MESHNET_DOCTOR_SHARD_END")


@pytest.mark.integration
@pytest.mark.skipif(
    not DOCTOR_MODEL,
    reason="set MESHNET_DOCTOR_MODEL (and optionally MESHNET_DOCTOR_SHARD_START/END) to run",
)
def test_doctor_smoke_runs_a_real_forward_on_a_real_model(tmp_path):
    cfg = {
        "model_hf_repo": DOCTOR_MODEL,
        "quantization": os.environ.get("MESHNET_DOCTOR_QUANTIZATION", "auto"),
        "download_dir": os.environ.get("MESHNET_DOWNLOAD_DIR") or None,
        "shard_start": DOCTOR_SHARD_START,
        "shard_end": int(DOCTOR_SHARD_END) if DOCTOR_SHARD_END else None,
        "force_cpu": os.environ.get("MESHNET_DOCTOR_CPU") == "1",
    }
    selection = resolve_selection(cfg)

    result = run_doctor(selection)

    report = result.reports[0]
    assert result.passed, f"doctor failed: {report.diagnostics}"
    assert report.status == STATUS_PASSED
    assert report.model.model_id == DOCTOR_MODEL
    assert report.duration_ms > 0
    assert report.model.config_fingerprint.startswith("sha256:")

    path = write_reports(result.reports, tmp_path / "capability.json")
    assert CapabilityReport.from_json(path.read_text()).passed
