"""
tests/test_config_wiring.py — Tests for config wiring fixes.

Covers:
  - extraction_enabled=False skips extraction
  - session_confidence_default is used when candidate confidence is 0
  - API key redaction works
  - Drift threshold alias properties return correct values
  - deprecation_cooling_days consistency
  - anti_compression_confidence fallback matches config default
"""
from __future__ import annotations

import asyncio
import logging
from dataclasses import dataclass, field
from typing import Any, List, Optional
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

# ---------------------------------------------------------------------------
# Config tests
# ---------------------------------------------------------------------------


class TestDriftThresholdAliases:
    """drift_restore_score and drift_alert_score return the right values."""

    def test_default_values(self):
        from cortex.config import CortexConfig
        cfg = CortexConfig()
        assert cfg.drift_restore_score == 0.3  # alias for drift_check_threshold
        assert cfg.drift_alert_score == 0.5    # alias for drift_restore_threshold

    def test_custom_values(self):
        from cortex.config import CortexConfig
        cfg = CortexConfig(drift_check_threshold=0.25, drift_restore_threshold=0.6)
        assert cfg.drift_restore_score == 0.25
        assert cfg.drift_alert_score == 0.6

    def test_aliases_track_underlying_fields(self):
        from cortex.config import CortexConfig
        cfg = CortexConfig()
        # Mutate underlying field, alias should follow
        cfg.drift_check_threshold = 0.4
        assert cfg.drift_restore_score == 0.4


class TestDeprecationCoolingDaysConsistency:
    """All sources agree on deprecation_cooling_days = 14."""

    def test_dataclass_default(self):
        from cortex.config import CortexConfig
        cfg = CortexConfig()
        assert cfg.deprecation_cooling_days == 14

    def test_toml_defaults(self):
        from cortex.config import CortexConfig
        cfg = CortexConfig.from_toml()
        # grace_period_hours=336 → 336//24 = 14
        assert cfg.deprecation_cooling_days == 14


class TestExtractionEnabledConfig:
    """extraction_enabled parses from TOML and defaults correctly."""

    def test_default_true(self):
        from cortex.config import CortexConfig
        cfg = CortexConfig()
        assert cfg.extraction_enabled is True

    def test_from_toml_default(self):
        from cortex.config import CortexConfig
        cfg = CortexConfig.from_toml()
        assert cfg.extraction_enabled is True

    def test_overlay_false(self):
        from cortex.config import CortexConfig
        cfg = CortexConfig.from_toml(
            overlay={"cortex": {"extraction": {"enabled": False}}}
        )
        assert cfg.extraction_enabled is False


class TestExtractionGuardrailConfig:
    def test_defaults_loaded(self):
        from cortex.config import CortexConfig
        cfg = CortexConfig.from_toml()
        assert cfg.max_claims_per_extraction == 25
        assert cfg.max_entities_per_extraction == 100
        assert cfg.max_concurrent_extractions == 4

    def test_overlay_wiring(self):
        from cortex.config import CortexConfig
        cfg = CortexConfig.from_toml(
            overlay={
                "cortex": {
                    "extraction": {
                        "max_claims_per_extraction": 7,
                        "max_entities_per_extraction": 11,
                        "max_concurrent_extractions": 2,
                    }
                }
            }
        )
        assert cfg.max_claims_per_extraction == 7
        assert cfg.max_entities_per_extraction == 11
        assert cfg.max_concurrent_extractions == 2


class TestAntiCompressionFallback:
    """anti_compression_confidence fallback matches config default of 0.9."""

    def test_config_default(self):
        from cortex.config import CortexConfig
        cfg = CortexConfig()
        assert cfg.anti_compression_confidence == 0.9


# ---------------------------------------------------------------------------
# API key redaction tests
# ---------------------------------------------------------------------------


class TestRedactSecretsFilter:
    """RedactSecretsFilter masks API keys and bearer tokens."""

    def test_openai_key(self):
        from cortex.logging_utils import RedactSecretsFilter
        f = RedactSecretsFilter()
        assert f._redact("key is sk-abcdefghij1234567890") == "key is sk-*****"

    def test_anthropic_key(self):
        from cortex.logging_utils import RedactSecretsFilter
        f = RedactSecretsFilter()
        assert f._redact("using sk-ant-abc123def456ghi789") == "using sk-ant-*****"

    def test_personal_access_token(self):
        from cortex.logging_utils import RedactSecretsFilter
        f = RedactSecretsFilter()
        assert f._redact("token pa-abcdef123456789") == "token pa-*****"

    def test_key_prefix(self):
        from cortex.logging_utils import RedactSecretsFilter
        f = RedactSecretsFilter()
        assert f._redact("api key-mySecretTokenValue123") == "api key-*****"

    def test_cortex_api_key_env(self):
        from cortex.logging_utils import RedactSecretsFilter
        f = RedactSecretsFilter()
        result = f._redact("env: CORTEX_API_KEY=sk-something-secret")
        assert "CORTEX_API_KEY=*****" in result

    def test_bearer_token(self):
        from cortex.logging_utils import RedactSecretsFilter
        f = RedactSecretsFilter()
        result = f._redact("Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.xyz")
        assert "Bearer *****" in result
        assert "eyJ" not in result

    def test_no_false_positive_on_short_strings(self):
        from cortex.logging_utils import RedactSecretsFilter
        f = RedactSecretsFilter()
        # Short "sk-" prefix without enough chars should not match
        assert f._redact("sk-short") == "sk-short"

    def test_xai_key(self):
        from cortex.logging_utils import RedactSecretsFilter
        f = RedactSecretsFilter()
        assert f._redact("xai-abcdefghij1234567890") == "xai-*****"

    def test_install_idempotent(self):
        from cortex.logging_utils import install_redaction_filter, RedactSecretsFilter
        test_logger = logging.getLogger("test_redaction_idempotent")
        # Install twice
        f1 = install_redaction_filter("test_redaction_idempotent")
        f2 = install_redaction_filter("test_redaction_idempotent")
        assert f1 is f2
        # Only one filter of this type
        count = sum(1 for f in test_logger.filters if isinstance(f, RedactSecretsFilter))
        assert count == 1

    def test_filter_in_logging_pipeline(self):
        """End-to-end: log a message with an API key, verify it's redacted."""
        from cortex.logging_utils import install_redaction_filter
        import io

        test_logger = logging.getLogger("test_redaction_e2e")
        test_logger.setLevel(logging.DEBUG)
        install_redaction_filter("test_redaction_e2e")

        # Capture log output
        handler = logging.StreamHandler(stream := io.StringIO())
        handler.setLevel(logging.DEBUG)
        test_logger.addHandler(handler)

        try:
            test_logger.info("Connecting with key sk-proj-abcdefghij1234567890")
            output = stream.getvalue()
            assert "sk-*****" in output
            assert "abcdefghij" not in output
        finally:
            test_logger.removeHandler(handler)


# ---------------------------------------------------------------------------
# Plugin-level tests: extraction_enabled and session_confidence_default
# ---------------------------------------------------------------------------


@dataclass
class _FakeConfig:
    """Minimal config stand-in for plugin tests."""
    extraction_enabled: bool = True
    session_confidence_default: float = 0.5
    owner_id: str = "test"
    storage_db_path: str = ":memory:"
    storage_enable_wal: bool = False
    storage_busy_timeout_ms: int = 1000
    extraction_model: str = "test-model"
    reconciliation_model: str = "test-model"
    embedding_model: str = "test-model"
    embedding_dim: int = 1536
    llm_provider_priority: list = field(default_factory=lambda: ["openai"])
    max_llm_calls_per_session: int = 0
    max_extraction_calls_per_batch: int = 10
    retrieval_token_budget: int = 4000
    extraction_custom_instructions: str = ""
    anti_compression_confidence: float = 0.9


class TestExtractionEnabledSkipsExtraction:
    """When extraction_enabled=False, remember() returns immediately."""

    @pytest.mark.asyncio
    async def test_extraction_disabled_skips(self):
        """remember() should return ok=True with 0 claims when disabled."""
        from cortex.api.plugin import CortexPlugin, RememberResult

        with patch.object(CortexPlugin, "__init__", lambda self, **kw: None):
            plugin = CortexPlugin.__new__(CortexPlugin)
            plugin.config = _FakeConfig(extraction_enabled=False)
            plugin._session_id = "test-session"
            plugin._llm_call_count = 0
            plugin._init_errors = []
            plugin.storage = MagicMock()
            plugin.storage.create_session = AsyncMock(return_value=MagicMock(id="db-pk"))
            plugin.storage.get_session_by_sid = AsyncMock(return_value=None)
            plugin.extractor = MagicMock()  # should NOT be called
            plugin.reconciler = MagicMock()
            plugin.entity_resolver = MagicMock()
            plugin.event_emitter = None

            result = await plugin.remember(
                conversation=[{"role": "user", "content": "Hello!"}],
                session_id="test-sess",
            )

            assert result.ok is True
            assert result.claims_extracted == 0
            assert result.claims_stored == 0
            # Extractor should NOT have been called
            plugin.extractor.extract.assert_not_called()


class TestSessionConfidenceDefault:
    """session_confidence_default is used when candidate has no confidence."""

    @pytest.mark.asyncio
    async def test_zero_confidence_uses_config_default(self):
        """_add_claim should use config default when candidate confidence is 0."""
        from cortex.core.reconciliation import ReconciliationEngine

        # Setup mocks
        storage = MagicMock()
        claim_mock = MagicMock(id="new-claim-id")
        storage.create_claim = AsyncMock(return_value=claim_mock)
        storage.add_provenance = AsyncMock()
        storage.log_changelog = AsyncMock()

        config = _FakeConfig(session_confidence_default=0.75)
        llm = MagicMock()
        entity_resolver = MagicMock()

        engine = ReconciliationEngine(
            storage=storage,
            config=config,
            llm=llm,
            entity_resolver=entity_resolver,
        )

        # Create a candidate with 0 confidence (i.e. LLM didn't assign one)
        candidate = MagicMock()
        candidate.subject = "Andrew"
        candidate.predicate = "likes"
        candidate.object_value = "coffee"
        candidate.confidence = 0.0  # zero = use default
        candidate.sensitivity = "normal"
        candidate.source_span = None

        result = await engine._add_claim(
            candidate=candidate,
            session_id="sess-1",
            owner_id="test",
        )

        # Verify storage.create_claim was called with the config default
        call_kwargs = storage.create_claim.call_args
        assert call_kwargs.kwargs["confidence"] == 0.75

    @pytest.mark.asyncio
    async def test_positive_confidence_kept(self):
        """_add_claim should keep non-zero candidate confidence as-is."""
        from cortex.core.reconciliation import ReconciliationEngine

        storage = MagicMock()
        claim_mock = MagicMock(id="new-claim-id")
        storage.create_claim = AsyncMock(return_value=claim_mock)
        storage.add_provenance = AsyncMock()
        storage.log_changelog = AsyncMock()

        config = _FakeConfig(session_confidence_default=0.75)
        llm = MagicMock()
        entity_resolver = MagicMock()

        engine = ReconciliationEngine(
            storage=storage,
            config=config,
            llm=llm,
            entity_resolver=entity_resolver,
        )

        candidate = MagicMock()
        candidate.subject = "Andrew"
        candidate.predicate = "likes"
        candidate.object_value = "tea"
        candidate.confidence = 0.85  # explicit non-zero
        candidate.sensitivity = "normal"
        candidate.source_span = None

        await engine._add_claim(
            candidate=candidate,
            session_id="sess-1",
            owner_id="test",
        )

        call_kwargs = storage.create_claim.call_args
        assert call_kwargs.kwargs["confidence"] == 0.85
