from __future__ import annotations
import hashlib, importlib.util, json
from pathlib import Path
import pytest
SCRIPT=Path(__file__).parents[3]/"src/OID_analysis/scripts/plot_article_package.py"
SPEC=importlib.util.spec_from_file_location("article_plot",SCRIPT);assert SPEC and SPEC.loader
plot=importlib.util.module_from_spec(SPEC);SPEC.loader.exec_module(plot)
def sha(p:Path)->str:return hashlib.sha256(p.read_bytes()).hexdigest()
def test_data_extraction_schemas_and_exact_hashes():
 d=plot.load_package_data(plot.ROOT)
 assert len(d["reports"])==4 and all(r["fold_count"]==3 for r in d["reports"].values())
 assert d["audit"]["schema_id"]=="oid-phase4-audit/v3"
 assert d["summary"]["schema_id"]=="oid-phase4-compact-summary/v2"
 assert d["freeze"]["schema_id"]=="oid-conditional-freeze/v1"
 assert all(r["schema_id"]=="oid-phase3-5-loro/v2" for r in d["reports"].values())
def test_gate_margin_and_status():
 d=plot.load_package_data(plot.ROOT)
 for report in d["reports"].values():
  for fold in report["folds"]:
   expected=fold["metric_weighted_r2"]-fold["realization_derangement_negative_control_metric_weighted_r2"]-fold["negative_control_margin"]
   assert plot.gate_margin(fold)==pytest.approx(expected)
   assert fold["negative_control_pass"]==(expected>0)
def test_generated_manifest_hash_binding_no_clobber_and_no_modes(tmp_path:Path):
 out=tmp_path/"package";plot.run(plot.ROOT,out)
 manifest=json.loads((out/"figure_manifest.json").read_text())
 assert manifest["schema_id"]=="oid-article-figure-package/v2"
 assert manifest["field_mode_assets"]==[] and "no field modes" in manifest["scope"]
 assert len(list(out.glob("[0-9][0-9]_*.png")))==7
 assert len(list(out.glob("[0-9][0-9]_*.pdf")))==7
 assert manifest["generator_inputs"][str(plot.SCRIPT)]==sha(plot.ROOT/plot.SCRIPT)
 assert manifest["generator_inputs"][str(plot.TEST)]==sha(plot.ROOT/plot.TEST)
 assert set(manifest)=={"schema_id","scope","cpu_only","no_clobber","field_mode_assets","evidence_inputs","generator_inputs","outputs"}
 for name,record in manifest["outputs"].items():
  assert sha(out/name)==record["sha256"]
  assert record["evidence_inputs"]==manifest["evidence_inputs"]
  assert record["generator_inputs"]==manifest["generator_inputs"]
 assert not any(any(token in p.name.lower() for token in ("field_mode","flow_field","spatial_mode")) for p in out.iterdir())
 with pytest.raises(FileExistsError):plot.run(plot.ROOT,out)
def test_missing_output_parent_fails(tmp_path:Path):
 with pytest.raises(FileNotFoundError):plot.run(plot.ROOT,tmp_path/"missing"/"package")

def test_formula_diagnostic_disclosures_and_source_footers():
 source=SCRIPT.read_text()
 assert r"C_{LR}^{-}=\Sigma_{ab}\Sigma_{bb}^{-1}" in source
 assert r"C_\tau^{\dagger}" not in source and "a_{LR}=C" not in source
 assert "training mean" in source and "coefficient-state reconstruction" in source
 for text in ("retained POD energy (fraction)","condition number (dimensionless)","period (samples)","minimum admissible shifts (count)","mean of 2 training estimates","minimum across 2 training records","p = 1/(999+1) = 0.001","Monte Carlo resolution floor"):
  assert text in source
 assert ".axhline(.05" not in source
 assert 'stem.startswith(("03_","04_","05_","06_"))' in source
 assert "Source: Phase 4 audit + formal LORO reports" in source
 assert 'f"{group}:sha256:{v} {k}"' in source
 assert "selection-adjusted, independently reselected realization-derangement control" in source
 assert "unweighted mean per-channel $R^2$" in source
 assert "no cross-realization force association" in source
