"""CAMB integration smoke test for SP(k).

This test exercises the vendored CAMB build under `dev/CAMB` and checks that enabling
`SPk_feedback` changes the non-linear matter power spectrum.

The test is automatically skipped if the CAMB shared library is not built.

Notes:
    - The CAMB Python package in `dev/CAMB` uses a local shared library file
      (`cambdll.dll` on Windows, `camblib.so` elsewhere). Importing `camb` will
      `sys.exit(...)` if the shared library is missing.
    - We run the check in a subprocess to avoid crashing the pytest process if a
      mismatched shared library is accidentally loaded.
"""

from __future__ import annotations

import os
import subprocess
import sys
import textwrap
from pathlib import Path

import pytest


def _repo_root() -> Path:
    return Path(__file__).resolve().parents[1]


def _camb_root() -> Path:
    return _repo_root() / "dev" / "CAMB"


def _camb_library_path() -> Path:
    camb_pkg_dir = _camb_root() / "camb"
    if sys.platform.startswith("win"):
        return camb_pkg_dir / "cambdll.dll"
    return camb_pkg_dir / "camblib.so"


def test_camb_spk_changes_nonlinear_matter_power() -> None:
    """Enabling SP(k) should change CAMB's non-linear matter power spectrum."""
    lib_path = _camb_library_path()
    if not lib_path.exists():
        pytest.skip(
            f"CAMB shared library not built at {lib_path}. "
            "Build it (e.g. in dev/CAMB: `python setup.py make`) to run this test."
        )

    # Run in a subprocess to avoid taking down pytest if something goes wrong at the
    # Python<->Fortran boundary.
    camb_root = str(_camb_root())
    code = textwrap.dedent(
        f"""
        import sys
        import numpy as np

        # Ensure we import the vendored CAMB, not a site-packages installation.
        sys.path.insert(0, {camb_root!r})

        try:
            import camb
        except SystemExit as exc:
            # CAMB exits if the shared library can't be found.
            raise RuntimeError(str(exc))


        def _run(spk_on: bool, relation_kind: int):
            pars = camb.CAMBparams()
            pars.set_cosmology(H0=67.5, ombh2=0.022, omch2=0.12, omk=0.0, mnu=0.0)
            pars.InitPower.set_params(As=2e-9, ns=0.965)

            # Matter power configuration (small, fast grid).
            pars.set_matter_power(redshifts=[0.0], kmax=5.0)
            pars.NonLinear = camb.model.NonLinear_both

            # Explicitly allocate and configure Halofit so settings are used during the run.
            pars.NonLinearModel = camb.Halofit()
            pars.NonLinearModel.set_params(
                halofit_version="mead2020",
                SPk_feedback=spk_on,
                SPk_relation_kind=relation_kind,
                # Relation kind 1 (power_law): safe simple parameters
                SPk_SO=200,
                SPk_fb_a=0.5,
                SPk_fb_pow=0.0,
                SPk_fb_pivot=1.0,
                # Relation kind 2 (cosmo_power_law): example parameters from README
                SPk_alpha=4.189,
                SPk_beta=1.273,
                SPk_gamma=0.298,
            )

            results = camb.get_results(pars)
            k, z, pk = results.get_matter_power_spectrum(minkh=0.1, maxkh=5.0, npoints=16)
            return np.asarray(k), np.asarray(z), np.asarray(pk)


        def _assert_changes(relation_kind: int):
            k0, z0, pk0 = _run(False, relation_kind)
            k1, z1, pk1 = _run(True, relation_kind)

            assert np.allclose(k0, k1)
            assert np.allclose(z0, z1)

            # Expect a measurable difference when SPk is enabled.
            ratio = pk1 / pk0
            diff = np.max(np.abs(ratio - 1.0))
            print("relation_kind", int(relation_kind), "max_abs_diff", float(diff))
            assert diff > 1e-6


        _assert_changes(1)
        _assert_changes(2)
        """
    ).lstrip()

    env = os.environ.copy()
    proc = subprocess.run(
        [sys.executable, "-c", code],
        cwd=str(_repo_root()),
        env=env,
        capture_output=True,
        text=True,
    )

    if proc.returncode != 0:
        # If something crashes (e.g. access violation), returncode will be non-zero.
        # Surface stdout/stderr for debugging.
        raise AssertionError(
            "CAMB SP(k) smoke test subprocess failed.\n"
            f"returncode: {proc.returncode}\n"
            f"stdout:\n{proc.stdout}\n"
            f"stderr:\n{proc.stderr}\n"
        )
