"""tests/test_config_status.py — Tests for GET /api/v1/admin/fleet/config-status.

Verifies:
- 200 response with expected schema
- All config sections present
- Sensitive fields are redacted (value == "***")
- Non-empty sensitive fields redacted; empty sensitive fields shown as ""
- Auth required (401 without key)
- Runtime info present
- Schema version present
- Field metadata shape is correct
"""
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from unittest.mock import AsyncMock, MagicMock

import pytest

pytest.importorskip("fastapi", reason="fastapi not installed")
pytest.importorskip("httpx", reason="httpx not installed")

from fastapi.testclient import TestClient  # noqa: E402


# ---------------------------------------------------------------------------
# Minimal fake config that covers all sections
# ---------------------------------------------------------------------------

@dataclass
class _FakeCortexConfig:
    # Storage
    storage_backend: str = "sqlite"
    storage_db_path: str = "cortex.db"
    storage_enable_wal: bool = True
    storage_busy_timeout_ms: int = 5000
    db_pool_max_open: int = 100
    db_pool_idle_timeout_seconds: float = 300.0
    db_pool_data_dir: str = "/data"

    # Embeddings
    embedding_model: str = "voyage-4-large"
    embedding_dim: int = 1024
    provider_embedding_provider: str = "voyage"
    provider_embedding_model: str = "voyage-4-large"
    provider_embedding_api_key: str = "sk-voyage-test-key"  # sensitive — must be redacted
    provider_embedding_base_url: str = ""
    provider_embedding_dim: int = 1024
    embedding_cache_enabled: bool = True
    embedding_cache_max_size: int = 1000
    embedding_cache_ttl_seconds: int = 3600

    # LLM providers
    provider_mode: str = "host"
    provider_llm_provider: str = "openai"
    provider_llm_model: str = "gemini-2.5-flash"
    provider_llm_api_key: str = "sk-openai-key"  # sensitive
    provider_llm_base_url: str = ""
    provider_extraction_base_url: str = ""
    provider_extraction_api_key: str = ""  # empty sensitive — show as ""
    provider_reconciliation_base_url: str = ""
    provider_reconciliation_api_key: str = ""
    provider_dialectic_base_url: str = ""
    provider_dialectic_api_key: str = ""
    provider_usage_enabled: bool = False
    provider_usage_tier: str = ""
    provider_usage_monthly_token_cap: int = 0
    provider_usage_monthly_embedding_cap: int = 0
    llm_provider_priority: List[str] = field(
        default_factory=lambda: ["google", "venice"]
    )

    # Extraction
    extraction_model: str = "gemini-2.5-flash"
    extraction_enabled: bool = True
    extraction_prompt_version: str = "v2"
    extraction_temporal_markers: bool = True
    coding_noise_filter: bool = True
    extract_assistant_turns: bool = True
    extraction_batch_size: int = 5
    extraction_custom_instructions: str = ""
    max_claims_per_extraction: int = 25
    max_entities_per_extraction: int = 100
    known_subjects: List[str] = field(default_factory=list)
    max_concurrent_extractions: int = 4

    # Reconciliation
    reconciliation_model: str = "gemini-2.5-flash"

    # Dialectic
    dialectic_model: Optional[str] = None

    # Retrieval
    retrieval_default_strategy: str = "hybrid"
    retrieval_max_tokens: int = 4000
    retrieval_vector_weight: float = 0.7
    retrieval_bm25_weight: float = 0.3
    retrieval_rerank_enabled: bool = True
    retrieval_rerank_model: str = "rerank-2.5-lite"
    retrieval_rerank_top_k: int = 5
    retrieval_rerank_initial_candidates: int = 30
    retrieval_confidence_threshold: float = 0.3
    retrieval_status_penalties: Optional[Dict[str, float]] = None
    retrieval_rrf_k: int = 60
    retrieval_recency_weight: float = 0.0
    retrieval_temporal_boost_weight: float = 0.15
    retrieval_max_chained_hops: int = 2

    # Token budgets
    token_budget_memory_fraction: float = 0.25
    token_budget_user_fraction: float = 0.55
    token_budget_reserve_fraction: float = 0.20
    token_budget_slot_priorities: List[str] = field(default_factory=list)
    cornerstone_token_budget: int = 800
    retrieval_token_budget: int = 4000
    total_memory_token_budget: int = 5000

    # Cornerstones
    cornerstone_auto_apply: bool = False
    max_cornerstone_count: int = 10
    cornerstone_drift_threshold: float = 0.05
    cornerstone_auto_apply_threshold: Optional[float] = None
    anti_compression_confidence: float = 0.9

    # Drift
    drift_check_threshold: float = 0.3
    drift_restore_threshold: float = 0.5

    # Circadian
    dream_cycle_hour_utc: int = 8
    dream_cycle_idle_timeout_sec: int = 3600
    dream_cycle_enabled: bool = True
    dream_cycle_timeout_sec: int = 600
    deep_dream_enabled: bool = False
    auto_sleep_after_minutes: int = 30
    fatigue_interval_sec: int = 300

    # Session
    session_confidence_default: float = 0.5
    promoted_confidence_default: float = 0.8
    session_fatigue_nap_threshold: float = 0.7
    session_fatigue_sleep_threshold: float = 0.9
    open_loop_stale_threshold: int = 5

    # Auth
    auth_jwt_secret: str = "super-secret-jwt"  # sensitive
    auth_api_key: str = "my-api-key"  # sensitive
    auth_require_auth: bool = True
    auth_public_endpoints: list = field(default_factory=list)

    # Rate limiting
    rate_limit_rpm: int = 60
    rate_limit_burst: int = 10

    # Working memory
    working_memory_enabled: bool = True
    working_memory_max_items: int = 100
    working_memory_default_ttl_seconds: int = 1800

    # Model routing
    model_routing_enabled: bool = False
    model_routing_fast: str = ""
    model_routing_main: str = ""
    model_routing_reasoning: str = ""
    model_routing_operation_map: dict = field(default_factory=dict)

    # Context compiler
    context_compiler_enabled: bool = True
    context_compiler_max_bundles: int = 7
    context_compiler_budget_overflow_strategy: str = "compress_then_drop"

    # Re-retrieval
    re_retrieval_enabled: bool = True
    re_retrieval_max_per_turn: int = 2
    re_retrieval_topic_shift_threshold: float = 0.7
    re_retrieval_min_budget_tokens: int = 500

    # Circuit breaker
    circuit_breaker_enabled: bool = True
    circuit_breaker_threshold: int = 5
    circuit_breaker_cooldown_sec: int = 300

    # Brain graph
    brain_graph_watch_dirs: List[str] = field(default_factory=list)
    brain_graph_include_patterns: List[str] = field(default_factory=list)
    brain_graph_exclude_patterns: List[str] = field(default_factory=list)
    brain_graph_debounce_seconds: float = 5.0

    # Metrics / quality / feature flags
    metrics_enabled: bool = True
    metrics_export_format: str = "json"
    quality_gates_enabled: bool = True
    feature_flags_enabled: bool = True

    # Webhooks
    webhook_allow_private_urls: bool = False

    # Aegis
    aegis_sentry_secret: str = "sentry-secret-value"  # sensitive

    # Deprecation
    deprecation_cooling_days: int = 14
    deprecation_archive_days: int = 90

    # API misc
    max_remember_body_bytes: int = 1_000_000
    max_page_size: int = 200
    log_retention_days: int = 90
    max_llm_calls_per_session: int = 1000

    # owner
    owner_id: str = "default"


def _make_mock_plugin() -> MagicMock:
    plugin = MagicMock()
    plugin.config = _FakeCortexConfig()
    return plugin


def _build_client(plugin: MagicMock, *, auth: bool = True) -> TestClient:
    from fastapi import FastAPI, HTTPException, APIRouter
    from cortex.api.routes.admin import router as shared_router, _make_routes

    # Use a fresh APIRouter to avoid test cross-contamination
    fresh_router = APIRouter()

    def _noop_verify(x: Any = None):
        return None

    async def _deny_verify(x: Any = None):
        raise HTTPException(status_code=401, detail="Unauthorized")

    verify_fn = _noop_verify if auth else _deny_verify

    from cortex.api.routes.admin.config_status import _register as _config_status_register
    _config_status_register(fresh_router, verify_fn)

    app = FastAPI()
    app.include_router(fresh_router)

    from cortex.api import deps as _deps
    _deps.plugin_holder["plugin"] = plugin

    return TestClient(app, raise_server_exceptions=False)


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

ENDPOINT = "/api/v1/admin/fleet/config-status"


class TestConfigStatusEndpoint:
    """Core tests for GET /api/v1/admin/fleet/config-status."""

    @pytest.fixture
    def client(self):
        return _build_client(_make_mock_plugin())

    def test_returns_200(self, client):
        resp = client.get(ENDPOINT)
        assert resp.status_code == 200

    def test_top_level_keys(self, client):
        resp = client.get(ENDPOINT)
        data = resp.json()
        for key in ("schema_version", "node", "version", "timestamp", "runtime", "config", "_meta"):
            assert key in data, f"Missing top-level key: {key!r}"

    def test_schema_version_present(self, client):
        resp = client.get(ENDPOINT)
        assert resp.json()["schema_version"] == "1.0"

    def test_version_matches_package(self, client):
        from cortex import __version__
        resp = client.get(ENDPOINT)
        assert resp.json()["version"] == __version__

    def test_runtime_keys(self, client):
        resp = client.get(ENDPOINT)
        runtime = resp.json()["runtime"]
        for key in ("python_version", "python_impl", "platform", "machine"):
            assert key in runtime, f"Missing runtime key: {key!r}"

    def test_timestamp_is_iso(self, client):
        from datetime import datetime
        resp = client.get(ENDPOINT)
        ts = resp.json()["timestamp"]
        assert isinstance(ts, str)
        datetime.fromisoformat(ts.replace("Z", "+00:00"))


class TestConfigStatusSections:
    """Config sections must all be present."""

    @pytest.fixture
    def config(self):
        client = _build_client(_make_mock_plugin())
        resp = client.get(ENDPOINT)
        assert resp.status_code == 200
        return resp.json()["config"]

    def test_storage_section_present(self, config):
        assert "storage" in config

    def test_embeddings_section_present(self, config):
        assert "embeddings" in config

    def test_llm_providers_section_present(self, config):
        assert "llm_providers" in config

    def test_extraction_section_present(self, config):
        assert "extraction" in config

    def test_reconciliation_section_present(self, config):
        assert "reconciliation" in config

    def test_retrieval_section_present(self, config):
        assert "retrieval" in config

    def test_auth_section_present(self, config):
        assert "auth" in config

    def test_circadian_section_present(self, config):
        assert "circadian" in config

    def test_cornerstones_section_present(self, config):
        assert "cornerstones" in config

    def test_drift_section_present(self, config):
        assert "drift" in config


class TestConfigStatusFieldMetadata:
    """Each field must have the required metadata keys."""

    @pytest.fixture
    def all_fields(self):
        client = _build_client(_make_mock_plugin())
        resp = client.get(ENDPOINT)
        config = resp.json()["config"]
        all_fields = []
        for section_fields in config.values():
            all_fields.extend(section_fields)
        return all_fields

    def test_field_has_name(self, all_fields):
        for f in all_fields:
            assert "name" in f, f"Field missing 'name': {f}"

    def test_field_has_value(self, all_fields):
        for f in all_fields:
            assert "value" in f, f"Field missing 'value': {f}"

    def test_field_has_default(self, all_fields):
        for f in all_fields:
            assert "default" in f, f"Field missing 'default': {f}"

    def test_field_has_type(self, all_fields):
        for f in all_fields:
            assert "type" in f, f"Field missing 'type': {f}"

    def test_field_has_env_var(self, all_fields):
        for f in all_fields:
            assert "env_var" in f, f"Field missing 'env_var': {f}"

    def test_field_has_sensitive_flag(self, all_fields):
        for f in all_fields:
            assert "sensitive" in f, f"Field missing 'sensitive': {f}"
            assert isinstance(f["sensitive"], bool)


class TestConfigStatusRedactionRegex:
    """B-CO1: Sensitive field redaction via regex — catches *_key, *_secret, etc."""

    def test_bare_key_suffix_detected(self):
        from cortex.api.routes.admin.config_status import _is_sensitive
        # Fields like voyage_key, openai_key should be sensitive
        assert _is_sensitive("voyage_key") is True
        assert _is_sensitive("openai_key") is True
        assert _is_sensitive("provider_key") is True

    def test_private_key_detected(self):
        from cortex.api.routes.admin.config_status import _is_sensitive
        assert _is_sensitive("private_key") is True
        assert _is_sensitive("ssh_private_key") is True

    def test_bare_secret_suffix_detected(self):
        from cortex.api.routes.admin.config_status import _is_sensitive
        assert _is_sensitive("api_secret") is True
        assert _is_sensitive("webhook_secret") is True

    def test_bare_token_suffix_detected(self):
        from cortex.api.routes.admin.config_status import _is_sensitive
        assert _is_sensitive("refresh_token") is True
        assert _is_sensitive("bearer_token") is True

    def test_false_positive_avoided(self):
        from cortex.api.routes.admin.config_status import _is_sensitive
        # "monkey", "keyboard" should NOT match
        assert _is_sensitive("monkey") is False
        assert _is_sensitive("keyboard") is False
        assert _is_sensitive("turkey") is False

    def test_non_sensitive_fields_unaffected(self):
        from cortex.api.routes.admin.config_status import _is_sensitive
        assert _is_sensitive("embedding_model") is False
        assert _is_sensitive("extraction_enabled") is False
        assert _is_sensitive("retrieval_max_tokens") is False

    def test_bare_key_suffix_redacted_in_response(self):
        """End-to-end: a field named 'voyage_key' should be redacted in the API response."""
        from dataclasses import dataclass, field as dc_field

        @dataclass
        class _ConfigWithBareKey(_FakeCortexConfig):
            voyage_key: str = "sk-voyage-leaked"

        plugin = MagicMock()
        plugin.config = _ConfigWithBareKey()
        client = _build_client(plugin)
        resp = client.get(ENDPOINT)
        config = resp.json()["config"]
        all_fields = {}
        for section_fields in config.values():
            for f in section_fields:
                all_fields[f["name"]] = f
        f = all_fields.get("voyage_key")
        assert f is not None, "voyage_key should appear in response"
        assert f["value"] == "***", f"Expected '***', got {f['value']!r}"
        assert f["sensitive"] is True


class TestConfigStatusRedaction:
    """Sensitive fields must be redacted."""

    @pytest.fixture
    def field_map(self):
        client = _build_client(_make_mock_plugin())
        resp = client.get(ENDPOINT)
        config = resp.json()["config"]
        result = {}
        for section_fields in config.values():
            for f in section_fields:
                result[f["name"]] = f
        return result

    def test_provider_llm_api_key_redacted(self, field_map):
        f = field_map.get("provider_llm_api_key")
        assert f is not None, "provider_llm_api_key not in response"
        assert f["value"] == "***", f"Expected '***', got {f['value']!r}"
        assert f["sensitive"] is True

    def test_auth_jwt_secret_redacted(self, field_map):
        f = field_map.get("auth_jwt_secret")
        assert f is not None, "auth_jwt_secret not in response"
        assert f["value"] == "***"
        assert f["sensitive"] is True

    def test_auth_api_key_redacted(self, field_map):
        f = field_map.get("auth_api_key")
        assert f is not None, "auth_api_key not in response"
        assert f["value"] == "***"
        assert f["sensitive"] is True

    def test_aegis_sentry_secret_redacted(self, field_map):
        f = field_map.get("aegis_sentry_secret")
        assert f is not None, "aegis_sentry_secret not in response"
        assert f["value"] == "***"
        assert f["sensitive"] is True

    def test_empty_sensitive_field_shown_as_empty_string(self, field_map):
        """provider_extraction_api_key is empty in fake config — show as '' not '***'."""
        f = field_map.get("provider_extraction_api_key")
        assert f is not None, "provider_extraction_api_key not in response"
        assert f["value"] == "", f"Expected '' for empty sensitive field, got {f['value']!r}"
        assert f["sensitive"] is True

    def test_voyage_api_key_redacted(self, field_map):
        f = field_map.get("provider_embedding_api_key")
        assert f is not None, "provider_embedding_api_key not in response"
        assert f["value"] == "***"

    def test_non_sensitive_field_not_redacted(self, field_map):
        f = field_map.get("storage_backend")
        assert f is not None, "storage_backend not in response"
        assert f["value"] == "sqlite"
        assert f["sensitive"] is False

    def test_extraction_model_not_redacted(self, field_map):
        f = field_map.get("extraction_model")
        assert f is not None, "extraction_model not in response"
        assert f["value"] == "gemini-2.5-flash"
        assert f["sensitive"] is False


class TestConfigStatusEnvVars:
    """Env var names must follow the CORTEX_ convention or known exceptions."""

    @pytest.fixture
    def field_map(self):
        client = _build_client(_make_mock_plugin())
        resp = client.get(ENDPOINT)
        config = resp.json()["config"]
        result = {}
        for section_fields in config.values():
            for f in section_fields:
                result[f["name"]] = f
        return result

    def test_storage_backend_env_var(self, field_map):
        f = field_map.get("storage_backend")
        assert f is not None
        assert f["env_var"] == "CORTEX_STORAGE_BACKEND"

    def test_auth_jwt_secret_special_env_var(self, field_map):
        f = field_map.get("auth_jwt_secret")
        assert f is not None
        assert f["env_var"] == "SUPABASE_JWT_SECRET"

    def test_auth_api_key_special_env_var(self, field_map):
        f = field_map.get("auth_api_key")
        assert f is not None
        assert f["env_var"] == "CORTEX_API_KEY"

    def test_provider_embedding_api_key_special_env_var(self, field_map):
        f = field_map.get("provider_embedding_api_key")
        assert f is not None
        assert f["env_var"] == "VOYAGE_API_KEY"


class TestConfigStatusSectionOrdering:
    """B-CO3: rerank fields must be in the 'rerank' section, not 'retrieval'."""

    @pytest.fixture
    def config(self):
        client = _build_client(_make_mock_plugin())
        resp = client.get(ENDPOINT)
        assert resp.status_code == 200
        return resp.json()["config"]

    def test_rerank_fields_in_rerank_section(self, config):
        """retrieval_rerank_* fields must land in the 'rerank' section."""
        assert "rerank" in config, "rerank section missing from config"
        rerank_names = {f["name"] for f in config["rerank"]}
        assert "retrieval_rerank_enabled" in rerank_names
        assert "retrieval_rerank_model" in rerank_names
        assert "retrieval_rerank_top_k" in rerank_names

    def test_rerank_fields_not_in_retrieval_section(self, config):
        """retrieval_rerank_* fields must NOT appear in the 'retrieval' section."""
        if "retrieval" not in config:
            return  # no retrieval section = fine
        retrieval_names = {f["name"] for f in config["retrieval"]}
        assert "retrieval_rerank_enabled" not in retrieval_names
        assert "retrieval_rerank_model" not in retrieval_names

    def test_section_for_field_rerank(self):
        from cortex.api.routes.admin.config_status import _section_for_field
        assert _section_for_field("retrieval_rerank_enabled") == "rerank"
        assert _section_for_field("retrieval_rerank_model") == "rerank"
        assert _section_for_field("retrieval_rerank_top_k") == "rerank"

    def test_section_for_field_retrieval(self):
        from cortex.api.routes.admin.config_status import _section_for_field
        assert _section_for_field("retrieval_default_strategy") == "retrieval"
        assert _section_for_field("retrieval_max_tokens") == "retrieval"


class TestConfigStatusAuth:
    """Auth guard tests."""

    def test_returns_401_without_key(self):
        from fastapi import FastAPI, APIRouter, HTTPException
        from cortex.api.routes.admin.config_status import _register

        fresh_router = APIRouter()

        async def _deny_verify(x: Any = None):
            raise HTTPException(status_code=401, detail="Unauthorized")

        _register(fresh_router, _deny_verify)

        app = FastAPI()
        app.include_router(fresh_router)

        plugin = _make_mock_plugin()
        from cortex.api import deps as _deps
        _deps.plugin_holder["plugin"] = plugin

        client = TestClient(app, raise_server_exceptions=False)
        resp = client.get(ENDPOINT)
        assert resp.status_code == 401

    def test_returns_200_with_valid_key(self):
        client = _build_client(_make_mock_plugin(), auth=True)
        resp = client.get(ENDPOINT)
        assert resp.status_code == 200

    def test_get_plugin_does_not_provide_auth(self):
        """B-CO2: get_plugin is a data dependency, NOT an auth gate.
        Auth must come from verify_api_key Depends(), not from get_plugin.
        This test verifies that even when get_plugin succeeds, auth still blocks."""
        from fastapi import FastAPI, APIRouter, HTTPException
        from cortex.api.routes.admin.config_status import _register
        from cortex.api import deps as _deps

        fresh_router = APIRouter()

        async def _deny_verify(x: Any = None):
            raise HTTPException(status_code=401, detail="Unauthorized")

        _register(fresh_router, _deny_verify)

        app = FastAPI()
        app.include_router(fresh_router)
        _deps.plugin_holder["plugin"] = _make_mock_plugin()

        client = TestClient(app, raise_server_exceptions=False)
        resp = client.get(ENDPOINT)
        # Even though plugin is available, auth denial blocks access
        assert resp.status_code == 401


class TestConfigStatusMeta:
    """_meta block must be self-documenting."""

    @pytest.fixture
    def meta(self):
        client = _build_client(_make_mock_plugin())
        resp = client.get(ENDPOINT)
        return resp.json()["_meta"]

    def test_description_present(self, meta):
        assert "description" in meta
        assert isinstance(meta["description"], str)

    def test_field_schema_present(self, meta):
        assert "field_schema" in meta
        schema = meta["field_schema"]
        for key in ("name", "value", "default", "type", "env_var", "sensitive"):
            assert key in schema, f"Missing field_schema key: {key!r}"
