"""Synthetic fixtures for the manifest-driven Phase 4 audit."""
from __future__ import annotations
import json
from pathlib import Path
import numpy as np
import pytest
from CCD_analysis.utils.run_contract import deterministic_json_bytes
from OID_analysis.v2.audit import (
    CODE_PATHS,
    NEGATIVE_CONTROL_MARGIN,
    OBSERVABLE_CHANNELS,
    OBSERVABLE_UNITS,
    RCOND,
    REALIZATIONS,
    SCENES,
    build_audit,
    run,
)
from OID_analysis.v2.manifest import sha256_file
from OID_analysis.v2.oid import _hash


def _write_json(path: Path, value: dict) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_bytes(deterministic_json_bytes(value))


def _repo(tmp_path: Path) -> tuple[Path, Path]:
    runs = tmp_path / "src/OID_analysis/runs"
    active = {"schema_id": "oid-active-evidence/v1", "formal_complete": {"status": "fail_closed", "blocker": "synthetic analyses not promoted", "complete_markers_created": 0},
              "scenes": {scene: {"realizations": [{"realization_id": rid} for rid in REALIZATIONS]} for scene in SCENES}}
    _write_json(runs / "ACTIVE_EVIDENCE.json", active)
    _write_json(tmp_path / "src/OID_analysis/provenance/contradictions/steady_reanalysis.json",
                {"near-body": {"rms": None}, "full-field": {"enstrophy_reduction": -2.0}, "force": {"Fx_reduction": -3.0}})
    for relative in CODE_PATHS:
        path = tmp_path / relative
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(f"# synthetic current source: {relative}\n")
    canonical = runs / "active-two-scene-v2/canonical"
    for scene in SCENES:
        fluid = 5025 if scene == "karman_re100" else 5024
        mask = np.zeros(5120, bool); mask[:fluid] = True; mask = mask.reshape(80, 64)
        metric = np.where(mask, 64.0, 0.0)
        for index, rid in enumerate(REALIZATIONS):
            directory = canonical / scene / rid; directory.mkdir(parents=True)
            n = 4
            arrays = {"dq_ctl": np.full((n, 10240), index + 1.0), "delta_forces": np.ones((n, 2)),
                      "delta_sensors": np.ones((n, 3)), "sample_ids": np.arange(n, dtype=np.int64),
                      "lattice_steps": np.arange(1, n + 1, dtype=np.int64), "times": np.arange(n, dtype=float),
                      "fluid_mask": mask, "cell_metric": metric, "state_weights": np.concatenate((metric.ravel(), metric.ravel()))}
            np.savez(directory / "paired_product.npz", **arrays)
            hashes = {key: _hash(value) for key, value in arrays.items()}
            sources = {name: {"content_hash_validated": True, "computed_content_sha256": "a" * 64,
                              "declared_content_sha256": "a" * 64, "file_sha256": "b" * 64}
                       for name in ("controlled_fields", "controlled_telemetry", "zero_baseline_fields", "zero_baseline_telemetry")}
            metadata = {"schema_id": "oid-canonical-paired/v1", "realization_id": rid, "product": "paired_product.npz",
                        "arrays": hashes, "canonical_pair_sources": sources, "pairing": "exact field-level IDs, lattice_steps, and times",
                        "timeline_contract": {"pairing": "controlled and baseline arrays exactly equal",
                                              **{f"{x}_sha256": hashes[x] for x in ("lattice_steps", "sample_ids", "times")}},
                        "field_shape": [80, 64], "fluid_mask_sha256": hashes["fluid_mask"], "cell_metric_sha256": hashes["cell_metric"],
                        "layout_contract": {"component_order": ["ux", "uy"]},
                        "metric_metadata": {"schema_id": "legacy-cell-metric/v1", "sampled_cell_area": 64.0,
                                            "quadrature": "uniform lattice-cell area, zero on actual solid mask cells"}}
            _write_json(directory / "paired_product.json", metadata)
    return tmp_path, canonical


def _analyses(repo: Path, canonical: Path, *, negative_pass: bool = True) -> None:
    root = repo / "src/OID_analysis/runs/active-two-scene-v2/analysis"
    for scene in SCENES:
        for observable, channels in OBSERVABLE_CHANNELS.items():
            directory = root / scene / observable; directory.mkdir(parents=True)
            model_arrays = {f"fold_{i}_propagator": np.eye(channels) for i in range(3)}
            np.savez(directory / "phase_3_5_models.npz", **model_arrays)
            model_hashes = {key: _hash(value) for key, value in model_arrays.items()}
            folds = []
            for i, rid in enumerate(REALIZATIONS):
                folds.append({"held_out_realization": rid, "training_realizations": [x for x in REALIZATIONS if x != rid],
                              "observable": observable, "requested_observable_rank": channels, "generated_rank": channels,
                              "propagator_condition_number": 3.0, "lr_identity_defect": 1e-12, "le_identity_defect": 1e-12,
                              "lr_projection_defect": 1e-12, "le_projection_defect": 1e-12, "per_channel_r2": [0.4] * channels,
                              "metric_weighted_r2": 0.4, "unweighted_mean_channel_r2": 0.4,
                              "tested_statistic": "metric_weighted_r2", "lr_state_coefficient_r2": -0.2 if i == 0 else 0.2,
                              "selection_adjusted_periodic_excluded_empirical_p_value": 0.002,
                              "circular_time_shift_surrogate_scores": [0.0] * 999,
                              "circular_time_shift_surrogate_statistic": "metric_weighted_r2",
                              "circular_time_shift_surrogate_selections":
                                  [{"surrogate_index": j, "offsets": [3, 3]} for j in range(999)],
                              "circular_time_shift_admissibility": [
                                  {"realization_id": training_id,
                                   "estimator": "deterministic normalized multichannel rFFT power spectrum",
                                   "period_samples": 10.0, "period_physical_time": 10.0,
                                   "peak_concentration": 0.8, "record_samples": 10,
                                   "exclusion_width_samples": 2,
                                   "exclusion_width_physical_time": 2.0,
                                   "integer_period_multiple_locations_samples": [10.0],
                                   "excluded_offsets": [0, 1, 2, 8, 9],
                                   "admissible_offsets": [3, 4, 5, 6, 7],
                                   "excluded_offset_count": 5, "admissible_offset_count": 5}
                                  for training_id in REALIZATIONS if training_id != rid],
                              "numerical_status": "truncated", "regularization": None, "rcond": RCOND,
                              "state_rank": channels, "state_retained_energy": 0.9,
                              "effective_observable_rank": channels,
                              "propagator_effective_rank": channels,
                              "propagator_threshold": 1e-12,
                              "propagator_smallest_retained_singular": 1.0,
                              "state_covariance_rank": channels,
                              "state_covariance_threshold": 1e-12,
                              "state_covariance_smallest_retained_singular": 1.0,
                              "realization_derangement_negative_control_metric_weighted_r2": 0.3 if negative_pass else 0.395,
                              "realization_derangement_selected_rank": channels,
                              "realization_derangement_selected_sample_lag": 0,
                              "realization_derangement_rank_lag_sensitivity":
                                  [{"rank": channels, "sample_lag": 0,
                                    "train_only_metric_weighted_r2": 0.1}],
                              "realization_derangement_selection":
                                  "complete training-only rank/sample-lag reselection on deranged training observables",
                              "negative_control_margin": NEGATIVE_CONTROL_MARGIN,
                              "negative_control_pass": negative_pass})
            report = {"schema_id": "oid-phase3-5-loro/v2", "fold_count": 3, "realization_ids": list(REALIZATIONS), "folds": folds,
                      "metrics": {"primary": "metric_weighted_r2", "supplementary": ["per_channel_r2", "unweighted_mean_channel_r2"]},
                      "circular_time_shift_null_contract": {"tested_statistic": "metric_weighted_r2",
                                                            "selection_repeated": "all training-only rank/sample-lag selection",
                                                            "periodic_equivalent_exclusions_applied": True,
                                                            "admissible_offsets_fixed_before_null_scoring": True,
                                                            "exclusion_width_samples": 2,
                                                            "exclusion_width_physical_time": 2.0,
                                                            "period_estimator": "deterministic normalized multichannel rFFT power spectrum",
                                                            "period_estimation_data": "each training realization's complete multichannel observable only; held-out realization excluded"},
                      "negative_control_gate": {"tested_statistic": "metric_weighted_r2",
                                                "deranged_selection_repeated": "all training-only rank/sample-lag selection",
                                                "all_folds_pass": negative_pass},
                      "numerical_contract": {"status": "truncated", "regularization": None,
                                             "rcond": RCOND,
                                             "applies_to": ["state covariance pseudoinverse",
                                                            "propagator generated rank",
                                                            "LR Gram rank and pseudoinverse",
                                                            "LE Gram rank and pseudoinverse"]},
                      "metric_contract": {"state_metric_sha256": "c" * 64, "observable_metric": [1.0] * 6,
                                          "observable_metric_sha256": _hash(np.ones(6)), "observable_metric_declaration": "H=I",
                                          "observable_metric_coordinates": "physical incremental observable channel coordinates",
                                          "observable_coordinate_units": OBSERVABLE_UNITS[observable]},
                      "state_pod_fit_count": 6, "state_pod_fit_bound": 18,
                      "state_field_transform_count": 12, "state_field_transform_bound": 12,
                      "physical_mode_materialization_count": 3, "physical_mode_materialization_bound": 3,
                      "execution_metadata": {"selection_adjusted": True, "surrogate_count": 999,
                                             "tested_statistic": "metric_weighted_r2",
                                             "negative_control_margin": NEGATIVE_CONTROL_MARGIN,
                                             "rcond": RCOND, "numerical_status": "truncated",
                                             "regularization": None,
                                             "code_sha256": {
                                                 relative: sha256_file(repo / relative)
                                                 for relative in CODE_PATHS},
                                             "observable_metric": [1.0] * 6,
                                             "observable_coordinate_units": OBSERVABLE_UNITS[observable],
                                             "cpu_only": True, "gpu_or_cfd_execution": False,
                                             "process_concurrency_limit": 3,
                                             "analysis_wall_time_seconds": 1.0, "peak_rss_kib": 100},
                      "input_products": [{"directory": str(canonical / scene / rid),
                                          "metadata_sha256": sha256_file(canonical / scene / rid / "paired_product.json"),
                                          "product_sha256": sha256_file(canonical / scene / rid / "paired_product.npz")} for rid in REALIZATIONS],
                      "model_artifact": {"path": "phase_3_5_models.npz", "sha256": sha256_file(directory / "phase_3_5_models.npz"),
                                         "array_sha256": model_hashes, "verified_after_write": True}}
            _write_json(directory / "phase_3_5_report.json", report)
            verification = {"schema_id": "oid-phase3-5-verification/v1",
                            "report_sha256": sha256_file(directory / "phase_3_5_report.json"),
                            "model_sha256": sha256_file(directory / "phase_3_5_models.npz"),
                            "model_array_sha256": model_hashes,
                            "report_json_verified": True, "model_npz_verified": True}
            _write_json(directory / "verification.json", verification)


def test_absent_formal_reports_are_cleanly_conditional(tmp_path):
    repo, _ = _repo(tmp_path)
    report = build_audit(repo)
    assert report["overall"] == "conditional"
    assert report["gates"]["canonical_products"]["status"] == "pass"
    assert report["gates"]["formal_analyses"]["status"] == "conditional"
    assert any("formal Phase 3-5" in claim for claim in report["prohibited_claims"])
    assert report["legacy_steady"]["status"] == "preserved_contradiction"


def test_complete_reports_are_strictly_audited_but_complete_stays_fail_closed(tmp_path):
    repo, canonical = _repo(tmp_path); _analyses(repo, canonical)
    report = build_audit(repo)
    assert report["overall"] == "conditional"
    assert report["gates"]["formal_analyses"]["status"] == "pass"
    assert report["gates"]["formal_complete"]["status"] == "conditional"
    assert all(job["status"] == "pass" for job in report["gates"]["formal_analyses"]["evidence"]["jobs"].values())
    assert any("cross-realization" in claim for claim in report["allowed_claims"])
    assert all(job["integrity_status"] == "pass" for job in report["gates"]["formal_analyses"]["evidence"]["jobs"].values())
    assert all(job["scientific_claim_status"] == "promoted" for job in report["gates"]["formal_analyses"]["evidence"]["jobs"].values())
    assert all(job["reconstruction_warning"] for job in report["gates"]["formal_analyses"]["evidence"]["jobs"].values())


def test_truthful_negative_control_failure_is_conditional_not_integrity_failure(tmp_path):
    repo, canonical = _repo(tmp_path); _analyses(repo, canonical, negative_pass=False)
    report = build_audit(repo)
    assert report["overall"] == "conditional"
    assert report["gates"]["formal_analyses"]["status"] == "conditional"
    jobs = report["gates"]["formal_analyses"]["evidence"]["jobs"]
    assert all(job["integrity_status"] == "pass" for job in jobs.values())
    assert all(job["scientific_claim_status"] == "not_promoted" for job in jobs.values())
    assert all(job["failed_folds"] == list(REALIZATIONS) for job in jobs.values())


def test_metric_integrity_failure_still_fails_audit(tmp_path):
    repo, canonical = _repo(tmp_path); _analyses(repo, canonical)
    path = repo / "src/OID_analysis/runs/active-two-scene-v2/analysis/karman_re100/delta_forces/phase_3_5_report.json"
    report = json.loads(path.read_text())
    report["metric_contract"]["observable_metric_declaration"] = "unknown"
    _write_json(path, report)
    verification_path = path.parent / "verification.json"
    verification = json.loads(verification_path.read_text())
    verification["report_sha256"] = sha256_file(path)
    _write_json(verification_path, verification)
    audit = build_audit(repo)
    assert audit["overall"] == "fail"
    job = audit["gates"]["formal_analyses"]["evidence"]["jobs"]["karman_re100/delta_forces"]
    assert job["integrity_status"] == "fail"
    assert job["scientific_claim_status"] == "not_evaluated"


def test_output_is_explicit_and_never_overwritten(tmp_path):
    repo, _ = _repo(tmp_path); output = tmp_path / "audit.json"
    run(repo, output)
    with pytest.raises(FileExistsError, match="refusing to overwrite"):
        run(repo, output)


@pytest.mark.parametrize("violation", ["code_hash", "periodic_exclusion", "rcond", "control_reselection"])
def test_audit_rejects_independent_review_contract_violations(tmp_path, violation):
    repo, canonical = _repo(tmp_path)
    _analyses(repo, canonical)
    path = repo / "src/OID_analysis/runs/active-two-scene-v2/analysis/karman_re100/delta_forces/phase_3_5_report.json"
    report = json.loads(path.read_text())
    if violation == "code_hash":
        report["execution_metadata"]["code_sha256"][CODE_PATHS[0]] = "0" * 64
    elif violation == "periodic_exclusion":
        report["circular_time_shift_null_contract"]["periodic_equivalent_exclusions_applied"] = False
    elif violation == "rcond":
        report["folds"][0]["rcond"] = 1e-8
    else:
        report["folds"][0].pop("realization_derangement_rank_lag_sensitivity")
    _write_json(path, report)
    verification_path = path.parent / "verification.json"
    verification = json.loads(verification_path.read_text())
    verification["report_sha256"] = sha256_file(path)
    _write_json(verification_path, verification)

    job = build_audit(repo)["gates"]["formal_analyses"]["evidence"]["jobs"][
        "karman_re100/delta_forces"
    ]
    assert job["integrity_status"] == "fail"
