"""ORCH-048: unit tests for the B6 registry-isolation verdict in staging_check.py.

B6 «Registry: sandbox present, prod ET/ORCH absent» is the staging-isolation
safety check. Its verdict logic is isolated into the pure function
``_evaluate_b6(known) -> (passed, detail)`` so both outcomes (clean staging
registry → PASS, polluted registry → FAIL) can be tested without standing up a
live staging instance or docker (02-trz §9, ADR-001).

These tests target that pure function plus the deterministic-degradation path
(``_run_b6``) and statically assert the host-path hack is gone (TR-6 / TC-06).
"""

import importlib.util
import pathlib

import pytest

# ---------------------------------------------------------------------------
# Load scripts/staging_check.py by path (scripts/ is not an importable package).
# ---------------------------------------------------------------------------
_SCRIPT_PATH = (
    pathlib.Path(__file__).resolve().parent.parent / "scripts" / "staging_check.py"
)


def _load_module():
    spec = importlib.util.spec_from_file_location("staging_check", _SCRIPT_PATH)
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


sc = _load_module()

SANDBOX = sc.SANDBOX_PROJECT_ID
PROD_ET = sc.PROD_ET_PROJECT_ID
PROD_ORCH = sc.PROD_ORCH_PROJECT_ID


# ---------------------------------------------------------------------------
# TC-01 — clean staging registry → PASS
# ---------------------------------------------------------------------------
def test_tc01_clean_registry_passes():
    passed, detail = sc._evaluate_b6({SANDBOX})
    assert passed is True
    assert "sandbox=YES" in detail
    assert "prod-ET=NO(good)" in detail
    assert "prod-ORCH=NO(good)" in detail


# ---------------------------------------------------------------------------
# TC-02 — prod-ET leaked into registry → FAIL
# ---------------------------------------------------------------------------
def test_tc02_prod_et_present_fails():
    passed, detail = sc._evaluate_b6({SANDBOX, PROD_ET})
    assert passed is False
    assert "sandbox=YES" in detail
    assert "prod-ET=YES(BAD!)" in detail
    assert "prod-ORCH=NO(good)" in detail


# ---------------------------------------------------------------------------
# TC-03 — prod-ORCH leaked into registry → FAIL
# ---------------------------------------------------------------------------
def test_tc03_prod_orch_present_fails():
    passed, detail = sc._evaluate_b6({SANDBOX, PROD_ORCH})
    assert passed is False
    assert "sandbox=YES" in detail
    assert "prod-ET=NO(good)" in detail
    assert "prod-ORCH=YES(BAD!)" in detail


# ---------------------------------------------------------------------------
# TC-04 — sandbox absent (empty registry) → deterministic FAIL, no exception
# ---------------------------------------------------------------------------
def test_tc04_empty_registry_fails_without_sandbox():
    passed, detail = sc._evaluate_b6(set())
    assert passed is False
    assert "sandbox=NO" in detail


# ---------------------------------------------------------------------------
# TC-05 — both prod projects leaked → FAIL
# ---------------------------------------------------------------------------
def test_tc05_both_prod_present_fails():
    passed, detail = sc._evaluate_b6({SANDBOX, PROD_ET, PROD_ORCH})
    assert passed is False
    assert "prod-ET=YES(BAD!)" in detail
    assert "prod-ORCH=YES(BAD!)" in detail


# ---------------------------------------------------------------------------
# TC-06 — registry source no longer depends on the host-path hack (TR-6)
# ---------------------------------------------------------------------------
def test_tc06_no_host_path_hack_in_source():
    source = _SCRIPT_PATH.read_text(encoding="utf-8")
    # The host-worktree path injection and the env-of-the-launcher reload that
    # caused the false FAIL must be gone from the B6 mechanics.
    assert 'sys.path.insert(0, "/repos/orchestrator")' not in source
    assert "importlib.reload" not in source


def test_tc06_registry_loader_uses_src_projects():
    # The verdict input is built from src.projects.known_plane_project_ids()
    # resolved via the running instance's own PYTHONPATH/env — not from a
    # host-path-injected import. We verify the loader delegates to that function.
    import src.projects as projects_mod

    sentinel = {"sentinel-id"}
    original = projects_mod.known_plane_project_ids
    projects_mod.known_plane_project_ids = lambda: sentinel
    try:
        known = sc._known_project_ids_from_registry()
    finally:
        projects_mod.known_plane_project_ids = original
    assert known == sentinel


# ---------------------------------------------------------------------------
# TC-07 — degraded registry source → deterministic FAIL (not false PASS, not raise)
# ---------------------------------------------------------------------------
def test_tc07_source_failure_is_deterministic_fail(monkeypatch):
    def _boom():
        raise RuntimeError("registry import blew up")

    monkeypatch.setattr(sc, "_known_project_ids_from_registry", _boom)

    results = sc.Results()
    # Must not raise.
    sc._run_b6(results)

    assert len(results._items) == 1
    label, passed, detail = results._items[0]
    assert passed is False
    assert "registry source unavailable" in detail
    assert "registry import blew up" in detail


# ---------------------------------------------------------------------------
# _run_b6 happy path wiring (clean registry → PASS result recorded)
# ---------------------------------------------------------------------------
def test_run_b6_records_pass_for_clean_registry(monkeypatch):
    monkeypatch.setattr(
        sc, "_known_project_ids_from_registry", lambda: {SANDBOX}
    )
    results = sc.Results()
    sc._run_b6(results)
    assert len(results._items) == 1
    _label, passed, detail = results._items[0]
    assert passed is True
    assert "sandbox=YES" in detail


# ---------------------------------------------------------------------------
# ORCH-061 / TC-03: the suite classifies checks as REAL vs SANDBOX_INFRA so the
# verdict (and exit-code) can tolerate KNOWN sandbox-infra FAILs (C9a/C9b) while
# staying fail-closed for real pipeline checks. Tested without a live stand.
# ---------------------------------------------------------------------------
from src.staging_verdict import REAL, SANDBOX_INFRA  # noqa: E402


def test_tc03_classify_infra_checks():
    """C9a/C9b classify as SANDBOX_INFRA; pipeline checks (A/B/C7/C8) as REAL."""
    assert sc._classify("C9a Branch appears in orchestrator-sandbox") == SANDBOX_INFRA
    assert sc._classify("C9b Analyst job enqueued in staging queue") == SANDBOX_INFRA
    assert sc._classify("C7 Create issue in Plane SANDBOX") == REAL
    assert sc._classify("C8 Trigger pipeline via /webhook/plane") == REAL
    assert sc._classify("A1 GET /health") == REAL
    assert sc._classify("B6 Registry: sandbox present") == REAL


def test_tc03_results_records_categories_and_keeps_tuple_shape():
    """Results.add auto-classifies each check; categorized_items() exposes the
    category WITHOUT changing the public 3-tuple shape of _items (ORCH-048 b6
    tests still unpack (label, passed, detail))."""
    results = sc.Results()
    results.add("C7 Create issue in Plane SANDBOX", True)
    results.add("C9a Branch appears in orchestrator-sandbox", False)

    # Public _items shape unchanged (regression guard for ORCH-048 tests).
    for item in results._items:
        assert len(item) == 3

    cats = {label: cat for label, _passed, cat in results.categorized_items()}
    assert cats["C7 Create issue in Plane SANDBOX"] == REAL
    assert cats["C9a Branch appears in orchestrator-sandbox"] == SANDBOX_INFRA


def test_tc03_explicit_category_overrides_autoclassify():
    """An explicit category arg is honoured (caller can force REAL)."""
    results = sc.Results()
    results.add("C9a Branch appears in orchestrator-sandbox", False, category=REAL)
    label, _passed, cat = results.categorized_items()[0]
    assert cat == REAL


def test_tc03_suite_verdict_waives_infra_only_failure():
    """End-to-end through the suite helpers: a run whose only failures are C9a/C9b
    -> exit 0 (waived) under tolerance; the waiver is surfaced for observability."""
    results = sc.Results()
    results.add("C7 Create issue in Plane SANDBOX", True)
    results.add("C8 Trigger pipeline via /webhook/plane", True)
    results.add("C9a Branch appears in orchestrator-sandbox", False)
    results.add("C9b Analyst job enqueued in staging queue", False)

    verdict = sc._verdict(results.categorized_items(), infra_tolerant=True)
    assert verdict.status == "SUCCESS"
    assert verdict.exit_code == 0
    assert len(verdict.waived) == 2

    # Strict mode (kill-switch off) re-fails the same run.
    strict = sc._verdict(results.categorized_items(), infra_tolerant=False)
    assert strict.exit_code == 1


def test_tc03_resolve_tolerance_strict_flag_forces_off():
    """--strict forces tolerance OFF regardless of the config default."""
    assert sc._resolve_tolerance(cli_strict=True) is False
