"""
TDD RED tests for the MLP weights artifact (save/load) and FEATURE_COLS contract
in strategies/strategy_mlp_scores.py.

Contract under test:
  - FEATURE_COLS: the 49 CSV column names in the exact order produced by
    strategies.strategy_activation_scores._prepare_features (which the MLP
    strategy reuses). This is the single source of truth for feature order.
  - save_mlp_artifact / load_mlp_artifact: JSON round-trip is bit-exact for
    float64 weights; load is cached by (path, mtime).
"""
import numpy as np
import pandas as pd
import pytest

from strategies.strategy_activation_scores import _prepare_features
from strategies.strategy_mlp_scores import (
    FEATURE_COLS,
    load_mlp_artifact,
    save_mlp_artifact,
)


def _random_layers(arch, seed=0):
    rng = np.random.default_rng(seed)
    return [
        (rng.normal(scale=0.5, size=(arch[i + 1], arch[i])),
         rng.normal(scale=0.1, size=arch[i + 1]))
        for i in range(len(arch) - 1)
    ]


class TestFeatureCols:
    def test_length_is_55(self):
        # 50-feature base + sopr_norm(32), cvd_norm(46,50), rvol_norm(51),
        # active1y_norm(52), hr_norm(53), sopr_norm(54) = 55 for bb50_bull arch
        assert len(FEATURE_COLS) == 55

    def test_order_matches_prepare_features(self):
        # Given a one-row df where each FEATURE_COLS column holds a unique value,
        # When _prepare_features is called with feature_cols=FEATURE_COLS (MLP path),
        # Then column i of the matrix equals the value of FEATURE_COLS[i].
        # Note: duplicates (cvd_norm@46+50, sopr_norm@32+54) produce equal values.
        values = {col: float(i + 1) / 100.0 for i, col in enumerate(FEATURE_COLS)}
        df = pd.DataFrame([values])
        from strategies.strategy_activation_scores import _prepare_features as _pf
        X = _pf(df, feature_cols=FEATURE_COLS)
        assert X.shape == (1, 55)
        expected = np.array([values[c] for c in FEATURE_COLS])
        np.testing.assert_array_equal(X[0], expected)

    def test_duplicates_are_intentional(self):
        # cvd_norm appears at cols 46 and 50; sopr_norm at cols 32 and 54.
        # These are intentional — the bb50_bull artifact was trained with this layout.
        assert len(set(FEATURE_COLS)) == 53  # 55 entries, 2 pairs of duplicates


class TestArtifactRoundTrip:
    def test_roundtrip_bit_exact(self, tmp_path):
        arch = [49, 16, 8, 1]
        layers = _random_layers(arch, seed=3)
        path = tmp_path / "mlp_weights_TEST_6H.json"

        save_mlp_artifact(str(path), layers, asset="COINBASE_BTCUSD", timeframe="6H")
        art = load_mlp_artifact(str(path))

        assert art["arch"] == arch
        assert art["feature_cols"] == FEATURE_COLS
        assert len(art["layers"]) == len(layers)
        for (W, b), (W2, b2) in zip(layers, art["layers"]):
            assert W2.dtype == np.float64 and b2.dtype == np.float64
            np.testing.assert_array_equal(W, W2)  # bit-exact via JSON repr
            np.testing.assert_array_equal(b, b2)

    def test_metadata_recorded(self, tmp_path):
        layers = _random_layers([49, 8, 4, 1], seed=4)
        path = tmp_path / "mlp_weights_META_1D.json"
        save_mlp_artifact(
            str(path), layers, asset="COINBASE_BTCUSD", timeframe="1D",
            training={"phase1": {"val_loss": 0.5}},
        )
        art = load_mlp_artifact(str(path))
        assert art["meta"]["asset"] == "COINBASE_BTCUSD"
        assert art["meta"]["timeframe"] == "1D"
        assert art["meta"]["training"]["phase1"]["val_loss"] == 0.5
        assert "activation" in art["meta"]

    def test_load_is_cached_until_file_changes(self, tmp_path):
        path = tmp_path / "mlp_weights_CACHE_4H.json"
        save_mlp_artifact(str(path), _random_layers([3, 2, 1], seed=5))
        first = load_mlp_artifact(str(path))
        assert load_mlp_artifact(str(path)) is first  # cached object

        # Rewrite with different weights and a newer mtime -> cache must refresh.
        import os
        import time
        time.sleep(0.01)
        save_mlp_artifact(str(path), _random_layers([3, 2, 1], seed=6))
        os.utime(str(path), (time.time() + 5, time.time() + 5))
        second = load_mlp_artifact(str(path))
        assert second is not first
        assert not np.array_equal(first["layers"][0][0], second["layers"][0][0])
