import os
from pathlib import Path

import numpy as np
import pytest

import nlsl

from nlsl.data import (
    fit_linear_baseline,
    natural_cubic_spline_resample,
    process_spectrum,
)


DEPRECATION_MATCH = "load_raw_datafile spline preprocessing is deprecated"


def _load_processed_with_public_api(model, processed, label):
    idx = model.generate_coordinates(
        processed.start,
        processed.stop,
        processed.y.size,
    )
    model.data = processed.y
    model.name(label, spectrum=idx)
    model.noise(processed.noise, spectrum=idx)
    return idx


def test_fit_linear_baseline_recovers_line():
    x = np.linspace(0.0, 9.0, 10)
    y = 2.0 + 0.5 * x
    corrected, intercept, slope, noise = fit_linear_baseline(x, y, edge_points=4)

    assert np.allclose(corrected, np.zeros_like(x))
    assert np.isclose(intercept, 2.0)
    assert np.isclose(slope, 0.5)
    assert np.isclose(noise, 0.0)


def test_process_spectrum_exposes_stop_coordinate(tmp_path):
    data_path = tmp_path / "sample.dat"
    x = np.linspace(-2.0, 3.0, 6)
    y = np.sin(x)
    with data_path.open("w", encoding="utf-8") as handle:
        for xi, yi in zip(x, y):
            handle.write(f"{xi: .8f} {yi: .8f}\n")

    processed = process_spectrum(
        data_path,
        10,
        0,
        derivative_mode=1,
        normalize=False,
    )

    assert processed.stop == pytest.approx(processed.x[-1])
    assert processed.stop == pytest.approx(
        processed.start + processed.step * (processed.y.size - 1)
    )


def test_natural_cubic_spline_resample_matches_fortran(tmp_path):
    x = np.linspace(-2.0, 2.0, 9)
    y = x**3 - 2.0 * x + 1.0
    nspline = 17
    data_path = tmp_path / "sample.dat"
    with data_path.open("w", encoding="utf-8") as handle:
        for xi, yi in zip(x, y):
            handle.write(f"{xi: .8f} {yi: .8f}\n")

    fortran = nlsl.nlsl()
    nlsl.fortrancore.parcom.nser = 1
    cwd = os.getcwd()
    try:
        os.chdir(tmp_path)
        fortran.procline(
            f"data sample ascii nspline {nspline} bc 0 deriv 1 noshift nonorm"
        )
    finally:
        os.chdir(cwd)
    expdat = nlsl.fortrancore.expdat
    ix0 = int(expdat.ixsp[0]) - 1
    count = int(expdat.npts[0])
    fortran_y = expdat.data[ix0 : ix0 + count].copy()

    _, python_y = natural_cubic_spline_resample(x, y, nspline)
    assert np.allclose(python_y, fortran_y)


def _capture_state():
    fc = nlsl.fortrancore
    expdat = fc.expdat
    mspctr = fc.mspctr
    lmcom = fc.lmcom

    state = {
        "data": expdat.data.copy(),
        "iform": expdat.iform.copy(),
        "ibase": expdat.ibase.copy(),
        "nft": expdat.nft.copy(),
        "rmsn": expdat.rmsn.copy(),
        "dataid": expdat.dataid.copy(),
        "wndoid": expdat.wndoid.copy(),
        "fvec": lmcom.fvec.copy(),
        "npts": expdat.npts.copy(),
        "ixsp": expdat.ixsp.copy(),
        "ishft": expdat.ishft.copy(),
        "idrv": expdat.idrv.copy(),
        "nrmlz": expdat.nrmlz.copy(),
        "sbi": expdat.sbi.copy(),
        "sdb": expdat.sdb.copy(),
        "srng": expdat.srng.copy(),
        "shft": expdat.shft.copy(),
        "tmpshft": expdat.tmpshft.copy(),
        "slb": expdat.slb.copy(),
        "sb0": expdat.sb0.copy(),
        "sphs": expdat.sphs.copy(),
        "spsi": expdat.spsi.copy(),
        "nspc": int(expdat.nspc),
        "ndatot": int(expdat.ndatot),
        "ishglb": int(expdat.ishglb),
        "normflg": int(expdat.normflg),
        "shftflg": int(expdat.shftflg),
        "bcmode": int(expdat.bcmode),
        "drmode": int(expdat.drmode),
        "nspline": int(expdat.nspline),
        "inform": int(expdat.inform),
    }
    for name in ("spectr", "wspec", "sfac"):
        if hasattr(mspctr, name):
            state[name] = getattr(mspctr, name).copy()
    return state


RUNTIME_STATE_KEYS = (
    "data",
    "iform",
    "nft",
    "rmsn",
    "dataid",
    "fvec",
    "npts",
    "ixsp",
    "ishft",
    "idrv",
    "sbi",
    "sdb",
    "srng",
    "shft",
    "tmpshft",
    "slb",
    "sb0",
    "sphs",
    "spsi",
    "nspc",
    "ndatot",
    "ishglb",
    "inform",
)


def test_load_data_matches_datac():
    samples = ["tests/sampl1", "tests/sampl3"]

    # Legacy path
    legacy = nlsl.nlsl()
    for sample in samples:
        cmd = f"data {sample} ascii nspline 200 bc 20 shift norm"
        legacy.procline(cmd)
    legacy_state = _capture_state()

    # Python path
    modern = nlsl.nlsl()
    modern.shift = True
    modern.derivative_mode = 1
    with pytest.warns(DeprecationWarning, match=DEPRECATION_MATCH) as caught:
        for sample in samples:
            modern.load_raw_datafile(
                sample, nspline=200, bc_points=20, normalize=True
            )
    assert len(caught) == len(samples)
    python_state = _capture_state()

    for key in RUNTIME_STATE_KEYS:
        legacy_value = legacy_state[key]
        python_value = python_state[key]
        if isinstance(legacy_value, np.ndarray):
            if np.issubdtype(legacy_value.dtype, np.floating):
                assert np.allclose(legacy_value, python_value)
            else:
                assert np.array_equal(legacy_value, python_value)
        else:
            assert legacy_value == python_value


def test_processed_data_sequence_matches_preprocessed_loader():
    sample_path = Path("tests/sampl1.dat")
    legacy = nlsl.nlsl()
    legacy.shift = True
    legacy.derivative_mode = 1

    cwd = os.getcwd()
    try:
        os.chdir(sample_path.parent)
        legacy.procline("data sampl1 ascii nspline 200 bc 20 shift nonorm")
    finally:
        os.chdir(cwd)
    legacy_state = _capture_state()

    modern = nlsl.nlsl()
    modern.shift = True
    modern.derivative_mode = 1
    processed = process_spectrum(
        sample_path,
        200,
        20,
        derivative_mode=modern.derivative_mode,
        normalize=False,
    )
    _load_processed_with_public_api(
        modern,
        processed,
        sample_path.stem,
    )
    processed_state = _capture_state()

    for key in RUNTIME_STATE_KEYS:
        legacy_value = legacy_state[key]
        processed_value = processed_state[key]
        if isinstance(legacy_value, np.ndarray):
            if np.issubdtype(legacy_value.dtype, np.floating):
                assert np.allclose(legacy_value, processed_value)
            else:
                assert np.array_equal(legacy_value, processed_value)
        else:
            assert legacy_value == processed_value
