"""Tests for src/micro_scanner/config.py — v2 field additions and KNOWN_KEYS validation."""

from __future__ import annotations

import textwrap

from src.micro_scanner.config import KNOWN_KEYS, MicroConfig, load_micro_config


class TestMicroConfigDefaults:
    """New v2 field defaults must match spec exactly."""

    def test_flood_threshold_default(self):
        cfg = MicroConfig()
        assert cfg.flood_threshold == 20

    def test_flood_promote_top_n_default(self):
        cfg = MicroConfig()
        assert cfg.flood_promote_top_n == 5

    def test_hysteresis_bonus_default(self):
        cfg = MicroConfig()
        assert cfg.hysteresis_bonus == 0.10

    def test_depth_gate_enabled_default(self):
        cfg = MicroConfig()
        assert cfg.depth_gate_enabled is True

    def test_config_version_default(self):
        cfg = MicroConfig()
        assert cfg.config_version == 1

    def test_entry_rejection_log_level_default(self):
        cfg = MicroConfig()
        assert cfg.entry_rejection_log_level == "debug"

    def test_gate_rollup_every_n_scans_default(self):
        cfg = MicroConfig()
        assert cfg.gate_rollup_every_n_scans == 1

    def test_audit_log_rejections_default(self):
        cfg = MicroConfig()
        assert cfg.audit_log_rejections is False

    def test_dry_run_signals_only_default(self):
        cfg = MicroConfig()
        assert cfg.dry_run_signals_only is True

    def test_composite_observer_enabled_default(self):
        cfg = MicroConfig()
        assert cfg.composite_observer_enabled is True

    def test_composite_observer_threshold_default(self):
        cfg = MicroConfig()
        assert cfg.composite_observer_threshold == 0.55

    def test_scan_deadline_seconds_default(self):
        cfg = MicroConfig()
        assert cfg.scan_deadline_seconds == 50.0

    def test_max_rejection_lines_per_scan_default(self):
        cfg = MicroConfig()
        assert cfg.max_rejection_lines_per_scan == 20

    def test_existing_fields_unchanged(self):
        """Existing pre-v2 defaults must be preserved byte-for-byte."""
        cfg = MicroConfig()
        assert cfg.enabled is False
        assert cfg.watchlist_max == 30
        assert cfg.score_entry_threshold == 0.55
        assert cfg.score_full_position == 0.70
        assert cfg.circuit_breaker_loss_pct == 8.0
        assert cfg.spread_max_pct == 0.3
        assert cfg.scoring_weights == {
            "volatility": 0.25,
            "momentum": 0.35,
            "liquidity": 0.25,
            "trend": 0.15,
        }


class TestKnownKeys:
    """KNOWN_KEYS must cover all MicroConfig fields."""

    def test_known_keys_is_frozenset(self):
        assert isinstance(KNOWN_KEYS, frozenset)

    def test_known_keys_contains_v2_fields(self):
        v2_fields = {
            "flood_threshold",
            "flood_promote_top_n",
            "hysteresis_bonus",
            "depth_gate_enabled",
            "entry_rejection_log_level",
            "gate_rollup_every_n_scans",
            "audit_log_rejections",
            "dry_run_signals_only",
            "composite_observer_enabled",
            "composite_observer_threshold",
            "config_version",
            "scan_deadline_seconds",
            "max_rejection_lines_per_scan",
        }
        assert v2_fields <= KNOWN_KEYS

    def test_known_keys_contains_existing_fields(self):
        assert "enabled" in KNOWN_KEYS
        assert "watchlist_max" in KNOWN_KEYS
        assert "circuit_breaker_loss_pct" in KNOWN_KEYS


class TestYamlLoading:
    """YAML overlay loading — valid keys, typo key warning, overrides."""

    def test_depth_gate_enabled_false_from_yaml(self, tmp_path):
        overlay = tmp_path / "micro.yaml"
        overlay.write_text("depth_gate_enabled: false\n")
        cfg = load_micro_config(config_path=str(overlay))
        assert cfg.depth_gate_enabled is False

    def test_config_version_override_from_yaml(self, tmp_path):
        overlay = tmp_path / "micro.yaml"
        overlay.write_text("config_version: 2\n")
        cfg = load_micro_config(config_path=str(overlay))
        assert cfg.config_version == 2

    def test_multiple_v2_overrides_from_yaml(self, tmp_path):
        overlay = tmp_path / "micro.yaml"
        overlay.write_text(
            textwrap.dedent("""\
                flood_threshold: 30
                flood_promote_top_n: 10
                hysteresis_bonus: 0.20
                depth_gate_enabled: false
                dry_run_signals_only: true
                composite_observer_threshold: 0.70
                config_version: 2
                scan_deadline_seconds: 45.0
                max_rejection_lines_per_scan: 15
            """)
        )
        cfg = load_micro_config(config_path=str(overlay))
        assert cfg.flood_threshold == 30
        assert cfg.flood_promote_top_n == 10
        assert cfg.hysteresis_bonus == 0.20
        assert cfg.depth_gate_enabled is False
        assert cfg.dry_run_signals_only is True
        assert cfg.composite_observer_threshold == 0.70
        assert cfg.config_version == 2
        assert cfg.scan_deadline_seconds == 45.0
        assert cfg.max_rejection_lines_per_scan == 15

    def test_typo_key_logs_warning_and_does_not_raise(self, tmp_path, caplog):
        """A typo key must emit unknown_config_keys warning and not raise."""
        overlay = tmp_path / "micro.yaml"
        # 'depth_gate_enable' is a plausible typo for 'depth_gate_enabled'
        overlay.write_text("depth_gate_enable: false\n")
        import structlog.testing

        with structlog.testing.capture_logs() as captured:
            cfg = load_micro_config(config_path=str(overlay))

        warning_events = [
            e for e in captured if e.get("event") == "unknown_config_keys"
        ]
        assert len(warning_events) == 1
        assert "depth_gate_enable" in warning_events[0]["keys"]
        # Config must still load; depth_gate_enabled keeps its default
        assert cfg.depth_gate_enabled is True

    def test_missing_yaml_uses_all_defaults(self, tmp_path):
        missing = tmp_path / "nonexistent.yaml"
        cfg = load_micro_config(config_path=str(missing))
        assert cfg.flood_threshold == 20
        assert cfg.config_version == 1
        assert cfg.depth_gate_enabled is True


class TestEnvVarOverrides:
    """Environment variable overrides for v2 fields."""

    def test_flood_threshold_env_override(self, tmp_path, monkeypatch):
        monkeypatch.setenv("MICRO_FLOOD_THRESHOLD", "50")
        overlay = tmp_path / "micro.yaml"
        overlay.write_text("")
        cfg = load_micro_config(config_path=str(overlay))
        assert cfg.flood_threshold == 50

    def test_depth_gate_enabled_env_false(self, tmp_path, monkeypatch):
        monkeypatch.setenv("MICRO_DEPTH_GATE_ENABLED", "false")
        overlay = tmp_path / "micro.yaml"
        overlay.write_text("")
        cfg = load_micro_config(config_path=str(overlay))
        assert cfg.depth_gate_enabled is False

    def test_config_version_env_override(self, tmp_path, monkeypatch):
        monkeypatch.setenv("MICRO_CONFIG_VERSION", "3")
        overlay = tmp_path / "micro.yaml"
        overlay.write_text("")
        cfg = load_micro_config(config_path=str(overlay))
        assert cfg.config_version == 3

    def test_env_overrides_yaml(self, tmp_path, monkeypatch):
        """Env takes precedence over YAML value."""
        monkeypatch.setenv("MICRO_FLOOD_PROMOTE_TOP_N", "99")
        overlay = tmp_path / "micro.yaml"
        overlay.write_text("flood_promote_top_n: 7\n")
        cfg = load_micro_config(config_path=str(overlay))
        assert cfg.flood_promote_top_n == 99

    def test_dry_run_signals_only_env_true(self, tmp_path, monkeypatch):
        monkeypatch.setenv("MICRO_DRY_RUN_SIGNALS_ONLY", "1")
        overlay = tmp_path / "micro.yaml"
        overlay.write_text("")
        cfg = load_micro_config(config_path=str(overlay))
        assert cfg.dry_run_signals_only is True

    def test_scan_deadline_seconds_env_override(self, tmp_path, monkeypatch):
        monkeypatch.setenv("MICRO_SCAN_DEADLINE_SECONDS", "30.5")
        overlay = tmp_path / "micro.yaml"
        overlay.write_text("")
        cfg = load_micro_config(config_path=str(overlay))
        assert cfg.scan_deadline_seconds == 30.5


class TestBigMoverConfigDefaults:
    """New Big-Mover field defaults (WU5) must match the spec."""

    def test_narrative_boost_categories_default(self):
        cfg = MicroConfig()
        assert cfg.narrative_boost_categories == ["layer-1", "defi", "layer-2"]

    def test_watchlist_filter_min_mc_default(self):
        cfg = MicroConfig()
        assert cfg.watchlist_filter_min_mc == 8_000_000.0

    def test_watchlist_filter_max_mc_default(self):
        cfg = MicroConfig()
        assert cfg.watchlist_filter_max_mc == 120_000_000.0

    def test_gate_squeeze_min_candles_default(self):
        cfg = MicroConfig()
        assert cfg.gate_squeeze_min_candles == 4

    def test_regime_fear_greed_pause_default(self):
        cfg = MicroConfig()
        assert cfg.regime_fear_greed_pause == 25.0

    def test_regime_reduced_score_threshold_default(self):
        cfg = MicroConfig()
        assert cfg.regime_reduced_score_threshold == 0.75

    def test_gate_ath_risk_exit_multiplier_default(self):
        cfg = MicroConfig()
        assert cfg.gate_ath_risk_exit_multiplier == 0.75


class TestBigMoverKnownKeys:
    """All Big-Mover fields must appear in KNOWN_KEYS."""

    def test_known_keys_contains_bigmover_fields(self):
        bigmover_fields = {
            "narrative_boost_categories",
            "watchlist_filter_min_mc",
            "watchlist_filter_max_mc",
            "watchlist_filter_vol_mc_ratio",
            "watchlist_filter_circ_supply_ratio",
            "watchlist_filter_mc_bypass_threshold",
            "gate_volmc_spot_ratio_threshold",
            "gate_volmc_prior_avg_max",
            "gate_squeeze_min_candles",
            "gate_squeeze_vol_pct",
            "gate_depth_slope_min",
            "gate_derivatives_futures_spot_ratio",
            "gate_derivatives_oi_pct_threshold",
            "gate_derivatives_funding_threshold",
            "gate_derivatives_score_bonus",
            "gate_tvl_change_threshold",
            "gate_tvl_score_bonus",
            "gate_ath_risk_exit_multiplier",
            "regime_fear_greed_pause",
            "regime_btc_price_change_halt",
            "regime_eth_btc_slope_reduced",
            "regime_btc_dom_advisory",
            "regime_reduced_score_threshold",
        }
        assert bigmover_fields <= KNOWN_KEYS


class TestBigMoverYamlFlattening:
    """Nested YAML sections must be flattened into flat MicroConfig fields."""

    def test_regime_section_flattened(self, tmp_path):
        import textwrap

        overlay = tmp_path / "micro.yaml"
        overlay.write_text(
            textwrap.dedent("""\
                regime:
                  fear_greed_pause: 20.0
                  btc_price_change_halt_pct: 7.5
                  btc_dom_advisory: 55.0
                  eth_btc_slope_reduced: 0.0
                  reduced_score_threshold: 0.80
            """)
        )
        cfg = load_micro_config(config_path=str(overlay))
        assert cfg.regime_fear_greed_pause == 20.0
        assert cfg.regime_btc_price_change_halt == 7.5
        assert cfg.regime_btc_dom_advisory == 55.0
        assert cfg.regime_reduced_score_threshold == 0.80

    def test_watchlist_filter_section_flattened(self, tmp_path):
        import textwrap

        overlay = tmp_path / "micro.yaml"
        overlay.write_text(
            textwrap.dedent("""\
                watchlist_filter:
                  min_mc: 5000000
                  max_mc: 80000000
                  vol_mc_ratio: 0.20
                  circ_supply_ratio: 0.40
                  mc_bypass_threshold: 15000000
            """)
        )
        cfg = load_micro_config(config_path=str(overlay))
        assert cfg.watchlist_filter_min_mc == 5_000_000
        assert cfg.watchlist_filter_max_mc == 80_000_000
        assert cfg.watchlist_filter_vol_mc_ratio == 0.20
        assert cfg.watchlist_filter_mc_bypass_threshold == 15_000_000

    def test_gates_section_flattened(self, tmp_path):
        import textwrap

        overlay = tmp_path / "micro.yaml"
        overlay.write_text(
            textwrap.dedent("""\
                gates:
                  squeeze_min_candles: 6
                  derivatives_score_bonus: 0.20
                  tvl_score_bonus: 0.12
                  ath_risk_exit_multiplier: 0.60
            """)
        )
        cfg = load_micro_config(config_path=str(overlay))
        assert cfg.gate_squeeze_min_candles == 6
        assert cfg.gate_derivatives_score_bonus == 0.20
        assert cfg.gate_tvl_score_bonus == 0.12
        assert cfg.gate_ath_risk_exit_multiplier == 0.60

    def test_nested_sections_no_unknown_key_warning(self, tmp_path):
        """Nested regime/watchlist_filter/gates sections must NOT emit unknown_config_keys."""
        import textwrap

        import structlog.testing

        overlay = tmp_path / "micro.yaml"
        overlay.write_text(
            textwrap.dedent("""\
                regime:
                  fear_greed_pause: 20.0
                watchlist_filter:
                  min_mc: 5000000
                gates:
                  squeeze_min_candles: 5
            """)
        )
        with structlog.testing.capture_logs() as captured:
            load_micro_config(config_path=str(overlay))

        warning_events = [
            e for e in captured if e.get("event") == "unknown_config_keys"
        ]
        assert not warning_events, (
            f"Unexpected unknown_config_keys warning for nested sections: {warning_events}"
        )
