"""
tests/test_config_completeness.py — Verify defaults.toml covers every CortexConfig field.

This test loads defaults.toml via CortexConfig.from_toml() (the same path
production code uses) and asserts that every field on the resulting config
object has a non-None value.  It also cross-checks specific values to confirm
that _from_raw() actually wires up the new TOML sections correctly (i.e.,
values are *sourced* from the TOML file, not silently falling back to the
dataclass defaults).
"""
from __future__ import annotations

from dataclasses import fields

import pytest

from cortex.config import CortexConfig, _DEFAULTS_PATH


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _cfg() -> CortexConfig:
    """Return config loaded purely from the package defaults.toml."""
    return CortexConfig.defaults()


# ---------------------------------------------------------------------------
# Core completeness test
# ---------------------------------------------------------------------------

class TestDefaultsTomlCompleteness:
    def test_defaults_toml_exists(self):
        """Sanity check: the defaults file is on disk."""
        assert _DEFAULTS_PATH.exists(), f"defaults.toml not found at {_DEFAULTS_PATH}"

    # Fields that are intentionally Optional[...] = None — their None value
    # MEANS something ("disabled", "unset") and is the correct default.
    # Do not add fields here just to pass the test; only fields where None is
    # the documented "disabled" sentinel belong in this list.
    _INTENTIONALLY_NONE_FIELDS: set = {
        "cornerstone_auto_apply_threshold",  # None = "always require manual review"
        "dialectic_model",  # None = "fall back to extraction_model" (opt-in override)
    }

    def test_every_field_is_non_none(self):
        """Every CortexConfig field must resolve to a non-None value from defaults.toml.

        Exception: fields in _INTENTIONALLY_NONE_FIELDS are explicitly designed
        to be None (meaning "disabled" or "unset") and are excluded from this check.
        """
        cfg = _cfg()
        none_fields = [
            f.name for f in fields(cfg)
            if getattr(cfg, f.name) is None
            and f.name not in self._INTENTIONALLY_NONE_FIELDS
        ]
        assert none_fields == [], (
            f"The following CortexConfig fields resolved to None after loading "
            f"defaults.toml — add them to the TOML file and wire them up in "
            f"_from_raw(): {none_fields}"
        )

    # ------------------------------------------------------------------ #
    # Section-by-section value checks (confirm _from_raw wiring, not just
    # dataclass defaults)
    # ------------------------------------------------------------------ #

    def test_storage_fields(self):
        cfg = _cfg()
        assert cfg.storage_backend == "sqlite"
        assert cfg.storage_db_path == "cortex.db"
        assert cfg.storage_enable_wal is True
        assert cfg.storage_busy_timeout_ms == 5000

    def test_embedding_fields(self):
        cfg = _cfg()
        assert cfg.embedding_model == "voyage-code-3"
        assert cfg.embedding_dim == 1024

    def test_extraction_fields(self):
        cfg = _cfg()
        assert cfg.extraction_model is not None
        assert isinstance(cfg.coding_noise_filter, bool)
        assert cfg.max_claims_per_extraction == 25
        assert cfg.max_entities_per_extraction == 100
        assert cfg.max_concurrent_extractions == 4

    def test_reconciliation_model(self):
        cfg = _cfg()
        assert cfg.reconciliation_model is not None
        assert len(cfg.reconciliation_model) > 0

    def test_token_budget_fields(self):
        cfg = _cfg()
        assert cfg.cornerstone_token_budget == 800
        assert cfg.retrieval_token_budget == 3000
        assert cfg.total_memory_token_budget == 4000
        # Invariant: total must be >= cornerstone + retrieval
        assert cfg.total_memory_token_budget >= (
            cfg.cornerstone_token_budget + cfg.retrieval_token_budget
        ), "total_memory_token_budget must be >= cornerstone + retrieval budgets"

    def test_drift_fields(self):
        cfg = _cfg()
        assert 0.0 < cfg.drift_check_threshold < 1.0
        assert cfg.drift_check_threshold < cfg.drift_restore_threshold <= 1.0
        assert 0.0 < cfg.anti_compression_confidence <= 1.0

    def test_brain_graph_fields(self):
        cfg = _cfg()
        assert isinstance(cfg.brain_graph_watch_dirs, list)
        assert len(cfg.brain_graph_watch_dirs) > 0
        assert isinstance(cfg.brain_graph_include_patterns, list)
        assert isinstance(cfg.brain_graph_exclude_patterns, list)
        assert cfg.brain_graph_debounce_seconds > 0

    def test_cornerstone_fields(self):
        cfg = _cfg()
        assert cfg.max_cornerstone_count > 0
        assert 0.0 < cfg.cornerstone_drift_threshold < 1.0
        assert isinstance(cfg.cornerstone_auto_apply, bool)

    def test_confidence_fields(self):
        cfg = _cfg()
        assert 0.0 <= cfg.session_confidence_default <= 1.0
        assert 0.0 <= cfg.promoted_confidence_default <= 1.0
        # Promoted confidence should be higher than session default
        assert cfg.promoted_confidence_default > cfg.session_confidence_default

    def test_fatigue_fields(self):
        cfg = _cfg()
        assert 0.0 < cfg.session_fatigue_nap_threshold < 1.0
        assert cfg.session_fatigue_nap_threshold < cfg.session_fatigue_sleep_threshold <= 1.0

    def test_circadian_fields(self):
        cfg = _cfg()
        assert cfg.auto_sleep_after_minutes >= 0
        assert isinstance(cfg.dream_cycle_enabled, bool)
        assert 0 <= cfg.dream_cycle_hour_utc <= 23
        assert cfg.dream_cycle_idle_timeout_sec > 0

    def test_retrieval_fields(self):
        cfg = _cfg()
        assert cfg.retrieval_default_strategy in ("hybrid", "semantic", "bm25")
        assert cfg.retrieval_max_tokens > 0
        assert 0.0 <= cfg.retrieval_vector_weight <= 1.0
        assert 0.0 <= cfg.retrieval_bm25_weight <= 1.0
        assert abs(cfg.retrieval_vector_weight + cfg.retrieval_bm25_weight - 1.0) < 1e-9, (
            "vector_weight + bm25_weight must equal 1.0"
        )

    def test_deprecation_fields(self):
        cfg = _cfg()
        assert cfg.deprecation_cooling_days > 0
        assert cfg.deprecation_archive_days > 0

    def test_owner_id(self):
        cfg = _cfg()
        assert cfg.owner_id is not None
        assert len(cfg.owner_id) > 0


# ---------------------------------------------------------------------------
# Promotion config validation (audit fix C-HIGH-3)
# ---------------------------------------------------------------------------

class TestPromotionConfigValidation:
    """Tests for validate_config() promotion weight/threshold checks."""

    def test_valid_promotion_weights_no_issues(self):
        from cortex.config import validate_config
        cfg = CortexConfig()  # defaults sum to 1.0
        issues = validate_config(cfg)
        assert not any("promotion" in i for i in issues)

    def test_promotion_weights_not_summing_to_one(self):
        from cortex.config import validate_config
        cfg = CortexConfig()
        object.__setattr__(cfg, "promotion_weight_evidence_strength", 0.9)
        issues = validate_config(cfg)
        assert any("promotion weights must sum to 1.0" in i for i in issues)

    def test_promotion_threshold_out_of_range_high(self):
        from cortex.config import validate_config
        cfg = CortexConfig()
        object.__setattr__(cfg, "promotion_threshold", 1.5)
        issues = validate_config(cfg)
        assert any("promotion_threshold" in i for i in issues)

    def test_promotion_threshold_out_of_range_low(self):
        from cortex.config import validate_config
        cfg = CortexConfig()
        object.__setattr__(cfg, "promotion_threshold", -0.1)
        issues = validate_config(cfg)
        assert any("promotion_threshold" in i for i in issues)

    def test_promotion_min_sessions_zero(self):
        from cortex.config import validate_config
        cfg = CortexConfig()
        object.__setattr__(cfg, "promotion_min_sessions", 0)
        issues = validate_config(cfg)
        assert any("promotion_min_sessions" in i for i in issues)

    def test_promotion_threshold_boundary_valid(self):
        from cortex.config import validate_config
        cfg = CortexConfig()
        object.__setattr__(cfg, "promotion_threshold", 0.0)
        issues = validate_config(cfg)
        assert not any("promotion_threshold" in i for i in issues)
        object.__setattr__(cfg, "promotion_threshold", 1.0)
        issues = validate_config(cfg)
        assert not any("promotion_threshold" in i for i in issues)


# ---------------------------------------------------------------------------
# Field count guard — catches newly added fields that aren't covered yet
# ---------------------------------------------------------------------------

class TestFieldCountGuard:
    # Update this number when new fields are intentionally added to CortexConfig.
    EXPECTED_FIELD_COUNT = 139  # 138 prior +1 retrieval_rrf_k (#574)

    def test_field_count_matches_expectation(self):
        """
        Guard against silent regression when new fields are added to CortexConfig
        without adding them to defaults.toml.  Update EXPECTED_FIELD_COUNT when
        you intentionally add new fields.
        """
        actual = len(fields(CortexConfig))
        assert actual == self.EXPECTED_FIELD_COUNT, (
            f"CortexConfig has {actual} fields but EXPECTED_FIELD_COUNT is "
            f"{self.EXPECTED_FIELD_COUNT}.  If you added new fields, also add "
            f"them to defaults.toml + _from_raw(), then update the constant."
        )
