"""Tests for secondary modules -- Coverage boost.

Covers:
  - security/sandbox.py (additional coverage for Docker/Namespace/JobObject mocked paths)
  - security/rate_limiter.py
  - security/credentials.py (additional coverage)
  - security/audit.py (additional coverage)
  - security/framework.py (additional coverage)
  - security/cicd_gate.py (additional coverage)
  - security/agent_vault.py (additional coverage)
  - security/sandbox_isolation.py (additional coverage)
  - browser/tools.py
  - browser/page_analyzer.py
  - browser/session_manager.py
  - cron/engine.py
  - cron/jobs.py
  - db/factory.py
  - forensics/replay_engine.py
"""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch

import pytest

# ============================================================================
# Security: Rate Limiter
# ============================================================================


class TestRateLimiter:
    def test_token_bucket_consume(self):
        from cognithor.security.rate_limiter import TokenBucket

        b = TokenBucket(rate=10.0, capacity=5.0)
        assert b.consume(1.0) is True
        assert b.consume(4.0) is True
        # Should be at 0 tokens now (consumed 5 total)
        assert b.consume(1.0) is False

    def test_token_bucket_refill(self):
        from cognithor.security.rate_limiter import TokenBucket

        b = TokenBucket(rate=1000.0, capacity=10.0)
        b.consume(10.0)  # drain
        # Force time advance
        b.last_refill -= 0.1  # 0.1s * 1000 rate = 100 tokens, capped at 10
        assert b.consume(1.0) is True

    @pytest.mark.asyncio
    async def test_rate_limiter_check(self):
        from cognithor.security.rate_limiter import RateLimiter

        # Very low refill rate so tokens don't refill between check() calls
        rl = RateLimiter(rate=0.001, capacity=5.0)
        for _ in range(5):
            assert await rl.check("client1") is True
        assert await rl.check("client1") is False

    @pytest.mark.asyncio
    async def test_rate_limiter_different_clients(self):
        from cognithor.security.rate_limiter import RateLimiter

        rl = RateLimiter(rate=100.0, capacity=2.0)
        assert await rl.check("a") is True
        assert await rl.check("b") is True

    @pytest.mark.asyncio
    async def test_rate_limiter_cleanup(self):
        from cognithor.security.rate_limiter import RateLimiter

        rl = RateLimiter(rate=10.0, capacity=10.0, cleanup_interval=0.0)
        await rl.check("old_client")
        # Force old timestamp
        rl._buckets["old_client"].last_refill -= 1000
        rl._last_cleanup = 0  # force cleanup on next check
        await rl.check("new_client")
        assert "old_client" not in rl._buckets


# ============================================================================
# Security: Credentials Store
# ============================================================================


class TestCredentialStore:
    def test_store_retrieve_delete(self, tmp_path):
        from cognithor.security.credentials import CredentialStore

        store = CredentialStore(
            store_path=tmp_path / "creds.enc",
            passphrase="test_passphrase_123",
        )
        entry = store.store("github", "token", "ghp_abc123")
        assert entry.service == "github"
        assert entry.key == "token"
        assert store.has("github", "token")
        val = store.retrieve("github", "token")
        assert val == "ghp_abc123"
        assert store.delete("github", "token")
        assert not store.has("github", "token")

    def test_agent_scoped_credentials(self, tmp_path):
        from cognithor.security.credentials import CredentialStore

        store = CredentialStore(
            store_path=tmp_path / "creds.enc",
            passphrase="test",
        )
        store.store("api", "key", "global_key")
        store.store("api", "key", "agent_key", agent_id="agent1")
        # Agent-specific should be found first
        assert store.retrieve("api", "key", agent_id="agent1") == "agent_key"
        # Global fallback
        assert store.retrieve("api", "key", agent_id="agent2") == "global_key"
        # Global direct
        assert store.retrieve("api", "key") == "global_key"

    def test_list_entries(self, tmp_path):
        from cognithor.security.credentials import CredentialStore

        store = CredentialStore(
            store_path=tmp_path / "creds.enc",
            passphrase="test",
        )
        store.store("s1", "k1", "v1")
        store.store("s2", "k2", "v2", agent_id="agent1")
        all_entries = store.list_entries()
        assert len(all_entries) == 2
        # Agent-filtered
        agent_entries = store.list_entries(agent_id="agent1")
        assert len(agent_entries) == 2  # agent1's + globals

    def test_inject_credentials(self, tmp_path):
        from cognithor.security.credentials import CredentialStore

        store = CredentialStore(
            store_path=tmp_path / "creds.enc",
            passphrase="test",
        )
        store.store("searxng", "api_key", "secret123")
        params = {"query": "test"}
        mapping = {"api_key": "searxng:api_key"}
        result = store.inject_credentials(params, mapping)
        assert result["api_key"] == "secret123"
        assert result["query"] == "test"

    def test_inject_invalid_mapping(self, tmp_path):
        from cognithor.security.credentials import CredentialStore

        store = CredentialStore(
            store_path=tmp_path / "creds.enc",
            passphrase="test",
        )
        result = store.inject_credentials({}, {"x": "invalid_no_colon"})
        assert "x" not in result

    def test_count_and_encrypted(self, tmp_path):
        from cognithor.security.credentials import CredentialStore

        store = CredentialStore(
            store_path=tmp_path / "creds.enc",
            passphrase="test",
        )
        assert store.count == 0
        store.store("s", "k", "v")
        assert store.count == 1
        assert store.is_encrypted is True

    def test_persistence(self, tmp_path):
        from cognithor.security.credentials import CredentialStore

        store1 = CredentialStore(
            store_path=tmp_path / "creds.enc",
            passphrase="test",
        )
        store1.store("s", "k", "v")

        # New instance should load from disk
        store2 = CredentialStore(
            store_path=tmp_path / "creds.enc",
            passphrase="test",
        )
        assert store2.retrieve("s", "k") == "v"

    def test_delete_agent_credential(self, tmp_path):
        from cognithor.security.credentials import CredentialStore

        store = CredentialStore(
            store_path=tmp_path / "creds.enc",
            passphrase="test",
        )
        store.store("s", "k", "v", agent_id="a1")
        assert store.delete("s", "k", agent_id="a1") is True
        assert store.delete("s", "k", agent_id="a1") is False

    def test_retrieve_nonexistent(self, tmp_path):
        from cognithor.security.credentials import CredentialStore

        store = CredentialStore(
            store_path=tmp_path / "creds.enc",
            passphrase="test",
        )
        assert store.retrieve("nope", "nope") is None

    def test_no_passphrase_raises(self, tmp_path, monkeypatch):
        from cognithor.security.credentials import CredentialStore

        monkeypatch.delenv("COGNITHOR_CREDENTIAL_KEY", raising=False)
        store = CredentialStore(
            store_path=tmp_path / "creds.enc",
            passphrase="",
        )
        # CredentialStore now auto-generates a passphrase via OS keyring when
        # none is supplied.  If keyring succeeds the store works without
        # raising; if keyring is unavailable, fernet is None and store() raises.
        if store._fernet is None:
            with pytest.raises(RuntimeError):
                store.store("s", "k", "v")
        else:
            # Keyring provided a key — store should succeed
            entry = store.store("s", "k", "v")
            assert entry.service == "s"


# ============================================================================
# Security: Sandbox (additional mocked paths)
# ============================================================================


class TestSandboxMocked:
    @pytest.mark.asyncio
    async def test_execute_docker_fallback_to_namespace(self):
        """Docker not available -> falls back to namespace -> then process."""
        from cognithor.security.sandbox import Sandbox

        sandbox = Sandbox()
        sandbox._capabilities = {
            "process": True,
            "jobobject": False,
            "bwrap": False,
            "nsjail": False,
            "docker": False,
        }
        # Mock _exec_process
        sandbox._exec_process = AsyncMock(
            return_value=MagicMock(
                exit_code=0,
                stdout="ok",
                stderr="",
                duration_ms=0,
                sandbox_level=MagicMock(value="process"),
                killed=False,
                timed_out=False,
            )
        )

        from cognithor.models import SandboxLevel

        await sandbox.execute("echo hello", level=SandboxLevel.CONTAINER)
        # Should have been downgraded
        sandbox._exec_process.assert_awaited()

    @pytest.mark.asyncio
    async def test_build_env(self):
        from cognithor.security.sandbox import Sandbox

        sandbox = Sandbox()
        env = sandbox._build_env({"CUSTOM": "val"})
        assert "CUSTOM" in env
        assert "PATH" in env

    def test_capabilities(self):
        from cognithor.security.sandbox import Sandbox

        sandbox = Sandbox()
        caps = sandbox.capabilities
        assert "process" in caps
        assert caps["process"] is True

    def test_available_levels(self):
        from cognithor.security.sandbox import Sandbox

        sandbox = Sandbox()
        levels = sandbox.available_levels
        assert len(levels) >= 1  # at minimum PROCESS

    def test_max_level(self):
        from cognithor.security.sandbox import Sandbox

        sandbox = Sandbox()
        ml = sandbox.max_level
        assert ml is not None


# ============================================================================
# DB: Factory
# ============================================================================


class TestDBFactory:
    def test_factory_sqlite(self, tmp_path):
        from cognithor.db.factory import create_backend

        config = MagicMock()
        config.database = None
        config.db_path = tmp_path / "test.db"
        backend = create_backend(config)
        assert backend is not None

    def test_factory_postgresql(self):
        db_config = MagicMock()
        db_config.backend = "postgresql"
        db_config.pg_host = "localhost"
        db_config.pg_port = 5432
        db_config.pg_dbname = "testdb"
        db_config.pg_user = "user"
        db_config.pg_password = "pass"
        db_config.pg_pool_min = 1
        db_config.pg_pool_max = 5
        config = MagicMock()
        config.database = db_config
        # This will import PostgreSQLBackend (which may fail without psycopg)
        # so we mock the import
        with patch("cognithor.db.factory.PostgreSQLBackend", create=True) as mock_pg:
            mock_pg.return_value = MagicMock()
            # Need to patch the import inside the function
            import cognithor.db.factory as factory_mod

            original_func = factory_mod.create_backend

            def patched_create(cfg):
                db_cfg = getattr(cfg, "database", None)
                if db_cfg and getattr(db_cfg, "backend", "") == "postgresql":
                    return MagicMock()  # mock PostgreSQL backend
                return original_func(cfg)

            with patch.object(factory_mod, "create_backend", patched_create):
                result = patched_create(config)
            assert result is not None

    def test_factory_unknown(self):
        from cognithor.db.factory import create_backend

        db_config = MagicMock()
        db_config.backend = "unknown_db"
        config = MagicMock()
        config.database = db_config
        with pytest.raises(ValueError):
            create_backend(config)


# ============================================================================
# Browser: Page Analyzer
# ============================================================================


class TestPageAnalyzer:
    @pytest.mark.asyncio
    async def test_analyze(self):
        from cognithor.browser.page_analyzer import PageAnalyzer

        analyzer = PageAnalyzer()
        page = AsyncMock()
        page.url = "https://example.com"
        page.title = AsyncMock(return_value="Test Page")
        page.evaluate = AsyncMock(
            return_value={
                "url": "https://example.com",
                "title": "Test Page",
                "textLength": 100,
                "htmlLength": 500,
                "links": [],
                "buttons": [],
                "inputs": [],
                "forms": [],
                "tables": [],
                "isLoaded": True,
            }
        )
        state = await analyzer.analyze(page)
        assert state.url == "https://example.com"

    @pytest.mark.asyncio
    async def test_analyze_error(self):
        from cognithor.browser.page_analyzer import PageAnalyzer

        analyzer = PageAnalyzer()
        page = AsyncMock()
        # Make page.url raise to trigger the outer except
        type(page).url = PropertyMock(side_effect=Exception("Page error"))
        state = await analyzer.analyze(page)
        assert len(state.errors) > 0

    @pytest.mark.asyncio
    async def test_detect_cookie_banner(self):
        from cognithor.browser.page_analyzer import PageAnalyzer

        analyzer = PageAnalyzer()
        page = AsyncMock()
        page.evaluate = AsyncMock(return_value={"found": False})
        result = await analyzer.detect_cookie_banner(page)
        assert result["found"] is False

    def test_stats(self):
        from cognithor.browser.page_analyzer import PageAnalyzer

        analyzer = PageAnalyzer()
        s = analyzer.stats()
        assert "analysis_count" in s


# ============================================================================
# Browser: Session Manager
# ============================================================================


class TestSessionManager:
    @pytest.mark.asyncio
    async def test_save_and_restore(self, tmp_path):
        from cognithor.browser.session_manager import SessionManager

        sm = SessionManager(storage_dir=str(tmp_path))
        page = AsyncMock()
        page.evaluate = AsyncMock(
            return_value={
                "cookies": [],
                "localStorage": {},
                "sessionStorage": {},
            }
        )
        context = AsyncMock()
        context.cookies = AsyncMock(
            return_value=[
                {"name": "sid", "value": "abc", "domain": ".example.com", "path": "/"},
            ]
        )
        page.context = context
        await sm.save_from_page(page, "session1")

        # Restore
        context2 = AsyncMock()
        context2.add_cookies = AsyncMock()
        await sm.restore_to_context(context2, "session1")

    def test_stats(self, tmp_path):
        from cognithor.browser.session_manager import SessionManager

        sm = SessionManager(storage_dir=str(tmp_path))
        s = sm.stats()
        assert isinstance(s, dict)


# ============================================================================
# Cron: Engine
# ============================================================================


class TestCronEngine:
    @pytest.mark.asyncio
    async def test_start_stop(self):
        from cognithor.cron.engine import CronEngine

        engine = CronEngine()
        await engine.start()
        assert engine.running is True
        await engine.stop()
        assert engine.running is False

    def test_properties(self):
        from cognithor.cron.engine import CronEngine

        engine = CronEngine()
        assert engine.running is False
        assert engine.job_count >= 0
        assert engine.has_enabled_jobs in (True, False)


# ============================================================================
# Cron: Jobs
# ============================================================================


class TestCronJobs:
    def test_load_jobs_no_file(self, tmp_path):
        from cognithor.cron.jobs import JobStore

        js = JobStore(path=str(tmp_path / "nonexistent.yaml"))
        jobs = js.load()
        # Should create defaults
        assert isinstance(jobs, dict)

    def test_load_jobs_with_file(self, tmp_path):
        import yaml

        jobs_yaml = tmp_path / "jobs.yaml"
        jobs_yaml.write_text(
            yaml.dump(
                {
                    "jobs": {
                        "test_job": {"schedule": "* * * * *", "prompt": "echo hi", "enabled": True},
                    }
                }
            )
        )
        from cognithor.cron.jobs import JobStore

        js = JobStore(path=str(jobs_yaml))
        jobs = js.load()
        assert len(jobs) >= 1


# ============================================================================
# Forensics: Replay Engine
# ============================================================================


class TestReplayEngine:
    def test_replay_run_no_plans(self):
        from cognithor.forensics.replay_engine import ReplayEngine

        gatekeeper = MagicMock()
        gatekeeper.evaluate_plan = MagicMock(return_value=[])
        gatekeeper.get_policies = MagicMock(return_value=[])
        gatekeeper.set_policies = MagicMock()
        engine = ReplayEngine(gatekeeper)

        run = MagicMock()
        run.session_id = "test_session"
        run.id = "run1"
        run.plans = []
        run.gate_decisions = []
        result = engine.replay_run(run)
        assert result is not None

    def test_counterfactual_analysis(self):
        from cognithor.forensics.replay_engine import ReplayEngine

        gatekeeper = MagicMock()
        gatekeeper.evaluate_plan = MagicMock(return_value=[])
        gatekeeper.get_policies = MagicMock(return_value=[])
        gatekeeper.set_policies = MagicMock()
        gatekeeper._parse_rule = MagicMock(return_value=MagicMock())
        engine = ReplayEngine(gatekeeper)

        run = MagicMock()
        run.session_id = "test_session"
        run.id = "run1"
        run.plans = []
        run.gate_decisions = []

        results = engine.counterfactual_analysis(
            run,
            {
                "strict": {"rules": [{"name": "block_all"}]},
                "permissive": {"rules": []},
            },
        )
        assert len(results) == 2


# ============================================================================
# Security: Audit
# ============================================================================


class TestAudit:
    def test_audit_trail_basic(self, tmp_path):
        from cognithor.security.audit import AuditTrail

        trail = AuditTrail(log_dir=tmp_path)
        h = trail.record_event("sess1", "test_action", {"key": "value"})
        assert h  # non-empty hash string
        assert trail.entry_count > 0

    def test_record_event_and_verify_chain(self, tmp_path):
        from cognithor.security.audit import AuditTrail

        trail = AuditTrail(log_dir=tmp_path)
        trail.record_event("s1", "login", {"user": "alice"})
        trail.record_event("s1", "logout", {"user": "alice"})
        valid, total, broken = trail.verify_chain()
        assert valid is True
        assert total == 2
        assert broken == -1

    def test_credential_masking(self, tmp_path):
        from cognithor.security.audit import AuditTrail, mask_credentials

        trail = AuditTrail(log_dir=tmp_path)
        # Log an event with credential-like data
        trail.record_event("sess1", "api_call", {"api_key": "sk-abcd12345678901234567890"})
        assert trail.entry_count > 0
        # Direct mask_credentials test
        masked = mask_credentials("Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9")
        assert "***" in masked

    def test_mask_dict(self):
        from cognithor.security.audit import mask_dict

        data = {"password": "secret", "nested": {"token": "Bearer abc123456789"}}
        masked = mask_dict(data)
        assert "***" in masked["nested"]["token"]

    def test_mask_dict_depth_limit(self):
        from cognithor.security.audit import mask_dict

        # depth > 10 returns data as-is
        data = {"key": "value"}
        result = mask_dict(data, depth=11)
        assert result == data

    def test_query_empty(self, tmp_path):
        from cognithor.security.audit import AuditTrail

        trail = AuditTrail(log_dir=tmp_path)
        results = trail.query(session_id="nonexistent")
        assert results == []

    def test_query_with_events(self, tmp_path):
        from cognithor.security.audit import AuditTrail

        trail = AuditTrail(log_dir=tmp_path)
        trail.record_event("sess1", "action_a", {"x": 1})
        trail.record_event("sess2", "action_b", {"x": 2})
        results = trail.query(session_id="sess1")
        assert len(results) == 1
        assert results[0]["session_id"] == "sess1"

    def test_verify_chain_empty(self, tmp_path):
        from cognithor.security.audit import AuditTrail

        trail = AuditTrail(log_dir=tmp_path)
        valid, total, broken = trail.verify_chain()
        assert valid is True
        assert total == 0

    def test_last_hash_and_log_path(self, tmp_path):
        from cognithor.security.audit import AuditTrail

        trail = AuditTrail(log_dir=tmp_path)
        assert trail.last_hash == "genesis"
        assert trail.log_path.exists() is False  # no entries yet
        trail.record_event("s1", "test")
        assert trail.last_hash != "genesis"

    def test_restore_chain_on_reinit(self, tmp_path):
        """Verify that a new AuditTrail restores the chain from disk."""
        from cognithor.security.audit import AuditTrail

        trail1 = AuditTrail(log_dir=tmp_path)
        trail1.record_event("s1", "ev1")
        last_hash = trail1.last_hash
        count = trail1.entry_count
        # Create a new trail from the same dir
        trail2 = AuditTrail(log_dir=tmp_path)
        assert trail2.last_hash == last_hash
        assert trail2.entry_count == count


# ============================================================================
# Security: Framework
# ============================================================================


class TestSecurityFramework:
    def test_incident_tracker_create_and_get(self):
        from cognithor.security.framework import (
            IncidentCategory,
            IncidentSeverity,
            IncidentTracker,
        )

        tracker = IncidentTracker()
        inc = tracker.create(
            "SQL Injection Attempt",
            IncidentCategory.PROMPT_INJECTION,
            IncidentSeverity.HIGH,
            description="Detected SQL in input",
        )
        assert inc.incident_id.startswith("INC-")
        assert tracker.get(inc.incident_id) is inc
        assert tracker.count == 1

    def test_incident_lifecycle(self):
        from cognithor.security.framework import (
            IncidentCategory,
            IncidentSeverity,
            IncidentStatus,
            IncidentTracker,
        )

        tracker = IncidentTracker()
        inc = tracker.create("Test", IncidentCategory.DATA_EXFILTRATION, IncidentSeverity.CRITICAL)
        tracker.transition(inc.incident_id, IncidentStatus.INVESTIGATING)
        assert inc.status == IncidentStatus.INVESTIGATING
        tracker.transition(inc.incident_id, IncidentStatus.CONTAINED)
        assert inc.contained_at != ""
        tracker.transition(inc.incident_id, IncidentStatus.RESOLVED)
        assert inc.resolved_at != ""
        tracker.transition(inc.incident_id, IncidentStatus.CLOSED)
        assert inc.closed_at != ""

    def test_incident_assign(self):
        from cognithor.security.framework import (
            IncidentCategory,
            IncidentSeverity,
            IncidentTracker,
        )

        tracker = IncidentTracker()
        inc = tracker.create("Test", IncidentCategory.DENIAL_OF_SERVICE, IncidentSeverity.MEDIUM)
        assert tracker.assign(inc.incident_id, "Alice", "analyst") is True
        assert inc.assigned_to == "Alice"
        assert tracker.assign("nonexistent", "Bob") is False

    def test_open_incidents_and_by_severity(self):
        from cognithor.security.framework import (
            IncidentCategory,
            IncidentSeverity,
            IncidentStatus,
            IncidentTracker,
        )

        tracker = IncidentTracker()
        i1 = tracker.create("A", IncidentCategory.CREDENTIAL_LEAK, IncidentSeverity.HIGH)
        tracker.create("B", IncidentCategory.CREDENTIAL_LEAK, IncidentSeverity.LOW)
        assert len(tracker.open_incidents()) == 2
        tracker.transition(i1.incident_id, IncidentStatus.RESOLVED)
        assert len(tracker.open_incidents()) == 1
        assert len(tracker.by_severity(IncidentSeverity.HIGH)) == 1
        assert len(tracker.by_category(IncidentCategory.CREDENTIAL_LEAK)) == 2

    def test_tracker_stats(self):
        from cognithor.security.framework import (
            IncidentCategory,
            IncidentSeverity,
            IncidentTracker,
        )

        tracker = IncidentTracker()
        tracker.create("X", IncidentCategory.BIAS_VIOLATION, IncidentSeverity.INFO)
        s = tracker.stats()
        assert s["total"] == 1
        assert isinstance(s["by_severity"], dict)

    def test_security_metrics(self):
        from cognithor.security.framework import (
            IncidentTracker,
            SecurityMetrics,
        )

        tracker = IncidentTracker()
        metrics = SecurityMetrics(tracker)
        assert metrics.mttd() == 0.0
        assert metrics.mttr() == 0.0
        assert metrics.resolution_rate() == 100.0
        assert metrics.incident_rate() == 0.0
        d = metrics.to_dict()
        assert isinstance(d, dict)
        assert "mttd_seconds" in d

    def test_posture_scorer_default(self):
        from cognithor.security.framework import PostureScorer

        scorer = PostureScorer()
        result = scorer.calculate()
        assert isinstance(result, dict)
        # Default has team_roles_filled=0 out of 6, so not 100%
        assert result["posture_score"] == 85.0
        assert result["level"] == "good"

    def test_posture_scorer_poor(self):
        from cognithor.security.framework import PostureScorer

        scorer = PostureScorer(mttr_threshold_seconds=100.0)
        result = scorer.calculate(
            resolution_rate=10.0,
            mttr_seconds=200.0,
            team_roles_filled=0,
            pipeline_pass_rate=10.0,
            compliance_score=10.0,
        )
        assert result["posture_score"] < 30
        assert result["level"] in ("poor", "critical")

    def test_security_team_auto_assign(self):
        from cognithor.security.framework import (
            IncidentCategory,
            IncidentSeverity,
            IncidentTracker,
            SecurityTeam,
            TeamMember,
            TeamRole,
        )

        team = SecurityTeam()
        member = TeamMember("M1", "Alice", TeamRole.SECURITY_ANALYST, on_call=True)
        team.add_member(member)
        assert team.member_count == 1
        assert len(team.on_call()) == 1
        tracker = IncidentTracker()
        inc = tracker.create("Test", IncidentCategory.PROMPT_INJECTION, IncidentSeverity.HIGH)
        assigned = team.auto_assign(inc)
        assert assigned is member
        assert inc.assigned_to == "Alice"

    def test_security_incident_time_properties(self):
        from cognithor.security.framework import (
            IncidentCategory,
            IncidentSeverity,
            SecurityIncident,
        )

        inc = SecurityIncident(
            incident_id="INC-1",
            title="Test",
            category=IncidentCategory.DENIAL_OF_SERVICE,
            severity=IncidentSeverity.HIGH,
            occurred_at="2025-01-01T00:00:00Z",
            detected_at="2025-01-01T00:01:00Z",
            resolved_at="2025-01-01T00:05:00Z",
        )
        assert inc.time_to_detect_seconds == 60.0
        assert inc.time_to_resolve_seconds == 240.0
        d = inc.to_dict()
        assert d["incident_id"] == "INC-1"


# ============================================================================
# Security: CICD Gate
# ============================================================================


class TestCICDGate:
    def test_security_gate_pass(self):
        from cognithor.security.cicd_gate import GateVerdict, SecurityGate

        gate = SecurityGate()
        result = gate.evaluate({})
        assert result.verdict == GateVerdict.PASS

    def test_security_gate_fail_critical(self):
        from cognithor.security.cicd_gate import GateVerdict, SecurityGate

        gate = SecurityGate()
        pipeline = {
            "stages": [
                {
                    "stage": "scan",
                    "result": "done",
                    "findings": [
                        {"severity": "critical", "title": "SQL injection"},
                    ],
                },
            ],
        }
        result = gate.evaluate(pipeline)
        assert result.verdict == GateVerdict.FAIL
        assert len(result.reasons) > 0

    def test_security_gate_override(self):
        from cognithor.security.cicd_gate import GateVerdict, SecurityGate

        gate = SecurityGate()
        result = gate.evaluate(
            {
                "stages": [
                    {"stage": "s1", "result": "ok", "findings": [{"severity": "critical"}]},
                ]
            }
        )
        assert result.verdict == GateVerdict.FAIL
        overridden = gate.override(result.gate_id, "admin", "hotfix required for production")
        assert overridden is not None
        assert overridden.verdict == GateVerdict.OVERRIDE
        assert gate.override("nonexistent", "admin", "this gate id does not exist") is None

    def test_history_and_pass_rate(self):
        from cognithor.security.cicd_gate import SecurityGate

        gate = SecurityGate()
        gate.evaluate({})
        gate.evaluate({})
        assert gate.pass_rate == 100.0
        h = gate.history()
        assert len(h) == 2
        assert gate.last_result() is not None

    def test_stats(self):
        from cognithor.security.cicd_gate import SecurityGate

        gate = SecurityGate()
        gate.evaluate({})
        s = gate.stats()
        assert isinstance(s, dict)
        assert s["total_evaluations"] == 1
        assert "policy" in s

    def test_gate_policy_to_dict(self):
        from cognithor.security.cicd_gate import GatePolicy

        policy = GatePolicy(policy_id="custom", block_on_critical=False)
        d = policy.to_dict()
        assert d["policy_id"] == "custom"
        assert d["block_on_critical"] is False

    def test_gate_result_to_dict(self):
        from cognithor.security.cicd_gate import SecurityGate

        gate = SecurityGate()
        result = gate.evaluate({})
        d = result.to_dict()
        assert "verdict" in d
        assert "gate_id" in d


# ============================================================================
# Security: Agent Vault
# ============================================================================


class TestAgentVault:
    def test_agent_vault_manager_create_and_get(self):
        from cognithor.security.agent_vault import AgentVaultManager

        mgr = AgentVaultManager()
        vault = mgr.create_vault("agent1")
        assert vault is not None
        assert mgr.get_vault("agent1") is vault
        assert mgr.vault_count == 1

    def test_vault_store_and_retrieve(self):
        from cognithor.security.agent_vault import AgentVault

        vault = AgentVault("agent1")
        secret = vault.store("my_api_key", "super_secret_value")
        assert secret.secret_id.startswith("SEC-")
        retrieved = vault.retrieve(secret.secret_id)
        assert retrieved == "super_secret_value"

    def test_vault_retrieve_nonexistent(self):
        from cognithor.security.agent_vault import AgentVault

        vault = AgentVault("agent1")
        assert vault.retrieve("nonexistent") is None

    def test_vault_rotate(self):
        from cognithor.security.agent_vault import AgentVault

        vault = AgentVault("agent1")
        secret = vault.store("key1", "old_value")
        rotated = vault.rotate(secret.secret_id, "new_value")
        assert rotated is not None
        assert rotated.rotation_count == 1
        assert vault.retrieve(secret.secret_id) == "new_value"

    def test_vault_revoke(self):
        from cognithor.security.agent_vault import AgentVault

        vault = AgentVault("agent1")
        secret = vault.store("key1", "val")
        assert vault.revoke(secret.secret_id) is True
        assert vault.retrieve(secret.secret_id) is None  # revoked
        assert vault.revoke("nonexistent") is False

    def test_vault_active_and_all_secrets(self):
        from cognithor.security.agent_vault import AgentVault

        vault = AgentVault("agent1")
        s1 = vault.store("k1", "v1")
        vault.store("k2", "v2")
        vault.revoke(s1.secret_id)
        # H-19: revoke() entfernt Secrets aus dem Tresor
        assert len(vault.active_secrets()) == 1
        assert len(vault.all_secrets()) == 1
        assert vault.secret_count == 1

    def test_vault_access_log(self):
        from cognithor.security.agent_vault import AgentVault

        vault = AgentVault("agent1")
        s = vault.store("k", "v")
        vault.retrieve(s.secret_id)
        log = vault.access_log()
        assert len(log) >= 2  # store + retrieve

    def test_vault_stats(self):
        from cognithor.security.agent_vault import AgentVault

        vault = AgentVault("agent1")
        vault.store("k", "v")
        s = vault.stats()
        assert s["total_secrets"] == 1
        assert s["active"] == 1

    def test_manager_destroy_vault(self):
        from cognithor.security.agent_vault import AgentVaultManager

        mgr = AgentVaultManager()
        vault = mgr.create_vault("agent1")
        vault.store("k1", "v1")
        assert mgr.destroy_vault("agent1") is True
        assert mgr.get_vault("agent1") is None
        assert mgr.destroy_vault("agent1") is False

    def test_manager_stats(self):
        from cognithor.security.agent_vault import AgentVaultManager

        mgr = AgentVaultManager()
        mgr.create_vault("a1")
        s = mgr.stats()
        assert s["total_vaults"] == 1


# ============================================================================
# Security: Sandbox Isolation
# ============================================================================


class TestSandboxIsolation:
    def test_tenant_manager_create_get(self):
        from cognithor.security.sandbox_isolation import TenantManager

        tm = TenantManager()
        tenant = tm.create("t1", "Test Tenant")
        assert tenant.tenant_id == "t1"
        assert tm.get("t1") is tenant
        assert tm.tenant_count == 1

    def test_tenant_manager_delete(self):
        from cognithor.security.sandbox_isolation import TenantManager

        tm = TenantManager()
        tm.create("t1", "Test Tenant")
        assert tm.delete("t1") is True
        assert tm.get("t1") is None
        assert tm.delete("t1") is False

    def test_tenant_agent_limits(self):
        from cognithor.security.sandbox_isolation import TenantManager

        tm = TenantManager()
        tm.create("t1", "Tenant", max_agents=2)
        assert tm.can_add_agent("t1") is True
        assert tm.add_agent("t1") is True
        assert tm.add_agent("t1") is True
        assert tm.add_agent("t1") is False  # at max
        assert tm.remove_agent("t1") is True
        assert tm.can_add_agent("t1") is True
        assert tm.can_add_agent("nonexistent") is False

    def test_tenant_stats(self):
        from cognithor.security.sandbox_isolation import TenantManager

        tm = TenantManager()
        tm.create("t1", "Tenant")
        s = tm.stats()
        assert isinstance(s, dict)
        assert s["total_tenants"] == 1

    def test_sandbox_manager_create_get(self):
        from cognithor.security.sandbox_isolation import SandboxManager, SandboxState

        sm = SandboxManager()
        sb = sm.create("agent1")
        assert sb.state == SandboxState.RUNNING
        assert sm.get(sb.sandbox_id) is sb
        assert sm.get_by_agent("agent1") is sb
        assert sm.sandbox_count == 1

    def test_sandbox_manager_terminate_suspend(self):
        from cognithor.security.sandbox_isolation import SandboxManager, SandboxState

        sm = SandboxManager()
        sb = sm.create("agent1")
        assert sm.suspend(sb.sandbox_id) is True
        assert sb.state == SandboxState.SUSPENDED
        assert sm.suspend(sb.sandbox_id) is False  # not running
        sb2 = sm.create("agent2")
        assert sm.terminate(sb2.sandbox_id) is True
        assert sb2.state == SandboxState.TERMINATED
        assert len(sm.running()) == 0

    def test_sandbox_tool_access(self):
        from cognithor.security.sandbox_isolation import AgentSandbox

        sb = AgentSandbox(
            sandbox_id="sb1",
            agent_id="a1",
            allowed_tools={"tool_a", "tool_b"},
            denied_tools={"tool_c"},
        )
        assert sb.check_tool_access("tool_a") is True
        assert sb.check_tool_access("tool_c") is False
        assert sb.check_tool_access("tool_d") is False  # not in allowed

    def test_sandbox_endpoint_access(self):
        from cognithor.security.sandbox_isolation import AgentSandbox

        sb = AgentSandbox(
            sandbox_id="sb1",
            agent_id="a1",
            allowed_endpoints={"https://api.example.com"},
        )
        assert sb.check_endpoint_access("https://api.example.com/v1") is True
        assert sb.check_endpoint_access("https://evil.com") is False

    def test_sandbox_resource_consumption(self):
        from cognithor.security.sandbox_isolation import AgentSandbox, ResourceLimit, ResourceType

        sb = AgentSandbox(
            sandbox_id="sb1",
            agent_id="a1",
            limits={ResourceType.CPU: ResourceLimit(ResourceType.CPU, 100.0)},
        )
        assert sb.consume_resource(ResourceType.CPU, 50.0) is True
        assert sb.consume_resource(ResourceType.CPU, 60.0) is False  # would exceed
        assert sb.consume_resource(ResourceType.MEMORY, 10.0) is True  # no limit

    def test_resource_limit_properties(self):
        from cognithor.security.sandbox_isolation import ResourceLimit, ResourceType

        rl = ResourceLimit(ResourceType.CPU, 100.0, current_value=75.0)
        assert rl.utilization == 75.0
        assert rl.exceeded is False
        rl.current_value = 110.0
        assert rl.exceeded is True
        d = rl.to_dict()
        assert d["resource"] == "cpu"

    def test_namespace_isolation(self):
        from cognithor.security.sandbox_isolation import NamespaceIsolation

        ni = NamespaceIsolation()
        ns = ni.create("agent1", "tenant1")
        assert ns.namespace_id == "tenant1:agent1"
        assert ni.get("agent1", "tenant1") is ns
        assert ni.validate_path("agent1", f"{ns.file_root}/somefile.txt", "tenant1") is True
        assert ni.validate_path("agent1", "/etc/passwd", "tenant1") is False
        assert ni.namespace_count == 1
        all_ns = ni.list_namespaces("tenant1")
        assert len(all_ns) == 1

    def test_isolation_enforcer_provision(self):
        from cognithor.security.sandbox_isolation import IsolationEnforcer

        enforcer = IsolationEnforcer()
        enforcer.tenants.create("default", "Default Tenant")
        result = enforcer.provision_agent("agent1")
        assert "sandbox_id" in result
        assert "namespace" in result

    def test_isolation_enforcer_decommission(self):
        from cognithor.security.sandbox_isolation import IsolationEnforcer

        enforcer = IsolationEnforcer()
        enforcer.tenants.create("default", "Default")
        enforcer.provision_agent("agent1")
        result = enforcer.decommission_agent("agent1")
        assert result["sandbox_terminated"] is True

    def test_isolation_enforcer_stats(self):
        from cognithor.security.sandbox_isolation import IsolationEnforcer

        enforcer = IsolationEnforcer()
        s = enforcer.stats()
        assert "sandboxes" in s
        assert "tenants" in s

    def test_admin_manager(self):
        from cognithor.security.sandbox_isolation import AdminManager, AdminRole

        am = AdminManager()
        admin = am.create("admin@test.com", "t1", AdminRole.TENANT_ADMIN)
        assert am.get(admin.admin_id) is admin
        assert am.check_permission(admin.admin_id, "manage_agents") is True
        assert am.check_permission(admin.admin_id, "nonexistent_perm") is False
        assert am.admin_count == 1
        assert am.revoke(admin.admin_id) is True
        assert am.admin_count == 0

    def test_delegated_admin_super(self):
        from cognithor.security.sandbox_isolation import AdminRole, DelegatedAdmin

        admin = DelegatedAdmin("a1", "admin@x.com", "t1", AdminRole.SUPER_ADMIN)
        assert admin.can("anything") is True
        d = admin.to_dict()
        assert d["role"] == "super_admin"
