from __future__ import annotations

import json
import shutil
import subprocess
from pathlib import Path

import numpy as np
import pytest

import ferro_ta

ROOT = Path(__file__).resolve().parents[2]
WASM_DIR = ROOT / "wasm"
PKG_JS = WASM_DIR / "pkg" / "ferro_ta_wasm.js"
SCRIPT = WASM_DIR / "conformance_node.js"


def _write_node_conformance_script(path: Path) -> None:
    path.write_text(
        """
const wasm = require("./node/ferro_ta_wasm.js");

function toArray(x) {
  return Array.from(x, (v) => (Number.isNaN(v) ? null : Number(v)));
}

const close = new Float64Array([44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.1, 45.42, 45.84, 46.08, 45.89, 46.03, 46.21, 46.02, 45.78]);
const high = new Float64Array([44.71, 44.5, 44.6, 44.09, 44.79, 45.2, 45.44, 45.73, 46.01, 46.44, 46.21, 46.39, 46.53, 46.3, 46.12]);
const low = new Float64Array([43.9, 43.8, 43.9, 43.2, 43.9, 44.2, 44.6, 44.8, 45.2, 45.5, 45.4, 45.5, 45.7, 45.6, 45.4]);
const volume = new Float64Array([1200, 1320, 1250, 1460, 1500, 1670, 1720, 1810, 1900, 2020, 1980, 2100, 2170, 2140, 2080]);

const payload = {
  sma: toArray(wasm.sma(close, 5)),
  ema: toArray(wasm.ema(close, 5)),
  wma: toArray(wasm.wma(close, 5)),
  rsi: toArray(wasm.rsi(close, 5)),
  adx: toArray(wasm.adx(high, low, close, 5)),
  mfi: toArray(wasm.mfi(high, low, close, volume, 5)),
};

process.stdout.write(JSON.stringify(payload));
""".strip()
        + "\n",
        encoding="utf-8",
    )


def _run_node_conformance() -> dict[str, list[float | None]]:
    if shutil.which("node") is None:
        pytest.skip("node is required for wasm/node conformance test")
    if not PKG_JS.exists():
        pytest.skip(
            "wasm/pkg not found; run `wasm-pack build --target nodejs --out-dir pkg`"
        )

    _write_node_conformance_script(SCRIPT)
    try:
        out = subprocess.check_output(
            ["node", str(SCRIPT)],
            cwd=WASM_DIR,
            text=True,
        )
    finally:
        if SCRIPT.exists():
            SCRIPT.unlink()
    return json.loads(out)


def _to_jsonable(arr: np.ndarray) -> list[float | None]:
    vals = np.asarray(arr, dtype=np.float64)
    return [None if np.isnan(x) else float(x) for x in vals]


def _assert_close_with_null_nan(
    actual: list[float | None],
    expected: list[float | None],
    *,
    atol: float,
) -> None:
    assert len(actual) == len(expected)
    a = np.array([np.nan if v is None else float(v) for v in actual], dtype=np.float64)
    e = np.array(
        [np.nan if v is None else float(v) for v in expected], dtype=np.float64
    )
    np.testing.assert_allclose(a, e, atol=atol, rtol=0.0, equal_nan=True)


def test_wasm_node_matches_python_core_indicators() -> None:
    close = np.array(
        [
            44.34,
            44.09,
            44.15,
            43.61,
            44.33,
            44.83,
            45.10,
            45.42,
            45.84,
            46.08,
            45.89,
            46.03,
            46.21,
            46.02,
            45.78,
        ],
        dtype=np.float64,
    )
    high = np.array(
        [
            44.71,
            44.50,
            44.60,
            44.09,
            44.79,
            45.20,
            45.44,
            45.73,
            46.01,
            46.44,
            46.21,
            46.39,
            46.53,
            46.30,
            46.12,
        ],
        dtype=np.float64,
    )
    low = np.array(
        [
            43.90,
            43.80,
            43.90,
            43.20,
            43.90,
            44.20,
            44.60,
            44.80,
            45.20,
            45.50,
            45.40,
            45.50,
            45.70,
            45.60,
            45.40,
        ],
        dtype=np.float64,
    )
    volume = np.array(
        [
            1200.0,
            1320.0,
            1250.0,
            1460.0,
            1500.0,
            1670.0,
            1720.0,
            1810.0,
            1900.0,
            2020.0,
            1980.0,
            2100.0,
            2170.0,
            2140.0,
            2080.0,
        ],
        dtype=np.float64,
    )

    node_payload = _run_node_conformance()

    py_expected = {
        "sma": _to_jsonable(ferro_ta.SMA(close, 5)),
        "ema": _to_jsonable(ferro_ta.EMA(close, 5)),
        "wma": _to_jsonable(ferro_ta.WMA(close, 5)),
        "rsi": _to_jsonable(ferro_ta.RSI(close, 5)),
        "adx": _to_jsonable(ferro_ta.ADX(high, low, close, 5)),
        "mfi": _to_jsonable(ferro_ta.MFI(high, low, close, volume, 5)),
    }

    for name, expected in py_expected.items():
        assert name in node_payload
        _assert_close_with_null_nan(node_payload[name], expected, atol=1e-9)
