"""
Regression tests for tools/mlp_results_table.py multi-asset global-state hazard.

Backlog item (observations.md): "No test for mlp_results_table.py with
non-BTC assets (default-arg bug was latent from day one)."

Contract under test
--------------------
`ASSET` and `TF_MAP` are two separate mutable module globals. `main()` keeps
them paired by reassigning both, in order, before evaluating any timeframe:

    ASSET  = args.asset
    TF_MAP = _tf_map(ASSET)

Any *other* caller of `evaluate_tf()` / `winner_path()` that updates one
global without the other can silently pair one asset's price data with a
different asset's optimized winner CSV — wrong economics, no error raised.
Confirmed empirically before the fix in this file: setting only
`mrt.ASSET = "COINBASE_ETHUSD"` (leaving the default BTC `TF_MAP` in place)
made `evaluate_tf("4H")` read `data/mlp/COINBASE_BTCUSD, 240.csv` (which
exists) together with the real
`results/winners/optimization_winner_strategy_mlp_scores_COINBASE_ETHUSD_4H.csv`
winner (which also exists) — both checks in `evaluate_tf` passed and it
proceeded into `strat.generate_signals` with mismatched asset data/params
(a `ValueError` in this particular BTC/ETH feature-column combination, but
nothing guarantees that in general — a compatible combination would silently
produce a wrong-but-plausible-looking result).

The fix: `evaluate_tf` no longer reads the module-global `TF_MAP` at all. It
resolves the asset once (explicit `asset=` kwarg, else the `ASSET` global)
and derives *both* the data file and the winner CSV path from that single
resolved value, so the two halves can no longer point at different assets
regardless of what `TF_MAP` happens to contain.

Only `COINBASE_BTCUSD` has real price CSVs under data/mlp/ in this repo, so
these tests exercise `_tf_map`, `winner_path`, and the global-pairing/
`evaluate_tf` guard directly rather than a full non-BTC backtest.
"""
import os

import pytest

import tools.mlp_results_table as mrt

REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))

NON_BTC_ASSET = "COINBASE_ETHUSD"  # has real winner CSVs, no real price data


class TestTfMap:
    """`_tf_map(asset)` must build asset-specific paths, not just BTC ones."""

    def test_returns_a_path_per_configured_timeframe(self):
        tf_map = mrt._tf_map(NON_BTC_ASSET)
        assert set(tf_map.keys()) == set(mrt._TF_PERIODS.keys())

    @pytest.mark.parametrize("tf,period", list(mrt._TF_PERIODS.items()))
    def test_path_embeds_the_requested_asset_and_period(self, tf, period):
        tf_map = mrt._tf_map(NON_BTC_ASSET)
        assert tf_map[tf] == f"data/mlp/{NON_BTC_ASSET}, {period}.csv"

    def test_different_assets_never_collide(self):
        btc = mrt._tf_map("COINBASE_BTCUSD")
        eth = mrt._tf_map(NON_BTC_ASSET)
        for tf in mrt._TF_PERIODS:
            assert btc[tf] != eth[tf]


class TestWinnerPath:
    """`winner_path(tf, asset)` must resolve to the real on-disk winner CSV."""

    @pytest.mark.parametrize("tf", ["4H", "6H", "8H", "12H", "1D"])
    def test_explicit_non_btc_asset_resolves_to_a_file_that_exists(self, tf):
        wp = mrt.winner_path(tf, NON_BTC_ASSET)
        assert wp == mrt.WINNER_DIR / f"optimization_winner_strategy_mlp_scores_{NON_BTC_ASSET}_{tf}.csv"
        abs_wp = os.path.join(REPO_ROOT, str(wp))
        assert os.path.exists(abs_wp), f"expected real fixture at {abs_wp}"

    def test_default_asset_falls_back_to_module_global(self, monkeypatch):
        monkeypatch.setattr(mrt, "ASSET", NON_BTC_ASSET)
        assert mrt.winner_path("4H") == mrt.winner_path("4H", NON_BTC_ASSET)


class TestGlobalPairingHazard:
    """
    The real hazard: ASSET and TF_MAP are independent mutable globals.
    main() pairs them correctly; a direct caller of evaluate_tf() need not.
    """

    def test_mains_paired_reassignment_keeps_asset_and_tf_map_in_sync(self, monkeypatch):
        # Given: simulate exactly what main() does for a non-BTC --asset,
        # i.e. reassign both globals together.
        monkeypatch.setattr(mrt, "ASSET", NON_BTC_ASSET)
        monkeypatch.setattr(mrt, "TF_MAP", mrt._tf_map(NON_BTC_ASSET))

        # Then: every TF's data-file entry and winner path agree on the asset.
        for tf in mrt._TF_PERIODS:
            assert NON_BTC_ASSET in mrt.TF_MAP[tf]
            assert NON_BTC_ASSET in str(mrt.winner_path(tf))

    def test_evaluate_tf_never_pairs_one_assets_data_with_anothers_winner(self, monkeypatch):
        # Given: a caller updates ASSET but forgets to re-pair TF_MAP — the
        # exact footgun the API shape invites. Leave TF_MAP at its BTC
        # default (real file, always exists) so the old code's first guard
        # ("data file missing") would NOT have caught this.
        monkeypatch.setattr(mrt, "ASSET", NON_BTC_ASSET)
        assert "COINBASE_BTCUSD" in mrt.TF_MAP["4H"]  # unpatched: still stale/default

        # Sanity-check the trap is real: the stale TF_MAP entry exists on disk,
        # and the winner CSV implied by the *new* ASSET also exists on disk —
        # so nothing before evaluate_tf's internals would short-circuit.
        stale_data_file = os.path.join(REPO_ROOT, mrt.TF_MAP["4H"])
        mismatched_winner = os.path.join(REPO_ROOT, str(mrt.winner_path("4H")))
        assert os.path.exists(stale_data_file)
        assert os.path.exists(mismatched_winner)
        assert NON_BTC_ASSET not in stale_data_file
        assert NON_BTC_ASSET in mismatched_winner

        # When: evaluate_tf resolves its own asset-consistent data file
        # (COINBASE_ETHUSD, which has no real price CSV in this repo) instead
        # of trusting the stale global TF_MAP.
        # Then: it must fail safe (skip, no data file) — never silently mix
        # the stale BTC data with the ETHUSD winner params.
        result = mrt.evaluate_tf("4H")
        assert result is None

    def test_evaluate_tf_ignores_tf_map_entirely_for_an_explicit_asset(self, monkeypatch):
        # Given: TF_MAP is left pointing at a completely unrelated asset.
        monkeypatch.setattr(mrt, "TF_MAP", {"4H": "does/not/exist.csv"})

        # When/Then: passing asset explicitly must not consult TF_MAP at all —
        # it still fails safe on the (real, expected) missing ETHUSD data file,
        # not on the bogus TF_MAP path.
        result = mrt.evaluate_tf("4H", asset=NON_BTC_ASSET)
        assert result is None
