"""Final coverage push -- targeting modules still below 90%.

Covers additional lines in:
  - a2a/types.py (Part types, Messages, Artifacts, Agent Card, streaming events)
  - browser/page_analyzer.py (element extraction, fuzzy_match, detect_cookie_banner)
  - browser/tools.py (vision tools remaining lines)
  - cron/engine.py (more heartbeat/scheduler paths, system jobs)
  - security/audit.py (record with AuditEntry, query filters, _entry_to_dict)
  - security/agent_vault.py (SessionFirewall, IsolatedSessionStore, VaultRotator)
  - security/cicd_gate.py (ContinuousRedTeam, WebhookNotifier, ScanSchedule, ScanScheduler)
  - graph/engine.py + graph/state.py (remaining uncovered lines)
"""

from __future__ import annotations

import json
from datetime import UTC
from unittest.mock import AsyncMock, MagicMock, patch

# ============================================================================
# A2A Types -- Part types, Messages, Artifacts, AgentCard, streaming events
# ============================================================================


class TestA2ATypes:
    def test_text_part_to_dict(self):
        from cognithor.a2a.types import TextPart

        p = TextPart(text="hello", metadata={"lang": "en"})
        d = p.to_dict()
        assert d["type"] == "text"
        assert d["text"] == "hello"
        assert d["metadata"]["lang"] == "en"

    def test_text_part_no_metadata(self):
        from cognithor.a2a.types import TextPart

        p = TextPart(text="hello")
        d = p.to_dict()
        assert "metadata" not in d

    def test_file_part_to_dict(self):
        from cognithor.a2a.types import FilePart

        p = FilePart(name="doc.pdf", uri="https://example.com/doc.pdf", data="AAAA")
        d = p.to_dict()
        assert d["type"] == "file"
        assert d["file"]["name"] == "doc.pdf"
        assert d["file"]["uri"] == "https://example.com/doc.pdf"
        assert d["file"]["bytes"] == "AAAA"

    def test_file_part_minimal(self):
        from cognithor.a2a.types import FilePart

        p = FilePart(name="x.txt")
        d = p.to_dict()
        assert "uri" not in d["file"]
        assert "bytes" not in d["file"]

    def test_data_part_to_dict(self):
        from cognithor.a2a.types import DataPart

        p = DataPart(data={"key": "val"}, metadata={"source": "test"})
        d = p.to_dict()
        assert d["type"] == "data"
        assert d["data"]["key"] == "val"
        assert d["metadata"]["source"] == "test"

    def test_data_part_no_metadata(self):
        from cognithor.a2a.types import DataPart

        p = DataPart(data={"x": 1})
        d = p.to_dict()
        assert "metadata" not in d

    def test_part_from_dict_text(self):
        from cognithor.a2a.types import TextPart, part_from_dict

        p = part_from_dict({"type": "text", "text": "hi"})
        assert isinstance(p, TextPart)
        assert p.text == "hi"

    def test_part_from_dict_file(self):
        from cognithor.a2a.types import FilePart, part_from_dict

        p = part_from_dict({"type": "file", "file": {"name": "x.txt", "uri": "http://x"}})
        assert isinstance(p, FilePart)
        assert p.name == "x.txt"

    def test_part_from_dict_data(self):
        from cognithor.a2a.types import DataPart, part_from_dict

        p = part_from_dict({"type": "data", "data": {"k": "v"}})
        assert isinstance(p, DataPart)

    def test_part_from_dict_unknown(self):
        from cognithor.a2a.types import TextPart, part_from_dict

        p = part_from_dict({"type": "unknown", "foo": "bar"})
        assert isinstance(p, TextPart)

    def test_task_state_properties(self):
        from cognithor.a2a.types import TaskState

        assert TaskState.COMPLETED.is_terminal is True
        assert TaskState.WORKING.is_terminal is False
        assert TaskState.SUBMITTED.is_active is True
        assert TaskState.COMPLETED.is_active is False
        assert TaskState.AUTH_REQUIRED.is_active is True

    def test_message_to_dict_and_from_dict(self):
        from cognithor.a2a.types import Message, MessageRole, TextPart

        msg = Message(
            role=MessageRole.USER,
            parts=[TextPart(text="Hello")],
            context_id="ctx1",
            task_id="t1",
            metadata={"key": "val"},
            extensions={"ext": "data"},
        )
        d = msg.to_dict()
        assert d["role"] == "user"
        assert d["contextId"] == "ctx1"
        assert d["taskId"] == "t1"
        assert d["metadata"]["key"] == "val"
        assert d["extensions"]["ext"] == "data"
        # from_dict
        msg2 = Message.from_dict(d)
        assert msg2.role == MessageRole.USER
        assert msg2.text == "Hello"

    def test_message_text_property(self):
        from cognithor.a2a.types import DataPart, Message, MessageRole, TextPart

        msg = Message(role=MessageRole.AGENT, parts=[DataPart(data={"x": 1})])
        assert msg.text == ""
        msg2 = Message(role=MessageRole.USER, parts=[TextPart(text="hi")])
        assert msg2.text == "hi"

    def test_artifact_to_dict_and_from_dict(self):
        from cognithor.a2a.types import Artifact, TextPart

        art = Artifact(
            parts=[TextPart(text="result")],
            name="output",
            description="The output",
            metadata={"format": "text"},
        )
        d = art.to_dict()
        assert d["name"] == "output"
        assert d["description"] == "The output"
        assert d["metadata"]["format"] == "text"
        art2 = Artifact.from_dict(d)
        assert art2.name == "output"

    def test_task_to_dict_full(self):
        from cognithor.a2a.types import (
            Artifact,
            Message,
            MessageRole,
            Task,
            TaskState,
            TextPart,
        )

        task = Task.create(message=Message(role=MessageRole.USER, parts=[TextPart(text="hi")]))
        task.artifacts.append(Artifact(parts=[TextPart(text="result")]))
        task.metadata = {"key": "val"}
        task.transition(TaskState.WORKING)
        d = task.to_dict()
        assert "messages" in d
        assert "artifacts" in d
        assert "metadata" in d
        assert "history" in d

    def test_task_status_update_event_sse(self):
        from cognithor.a2a.types import TaskState, TaskStatus, TaskStatusUpdateEvent

        evt = TaskStatusUpdateEvent(
            task_id="t1",
            context_id="c1",
            status=TaskStatus(state=TaskState.COMPLETED),
            final=True,
        )
        sse = evt.to_sse()
        assert "event: status" in sse
        assert "t1" in sse

    def test_task_artifact_update_event_sse(self):
        from cognithor.a2a.types import Artifact, TaskArtifactUpdateEvent, TextPart

        evt = TaskArtifactUpdateEvent(
            task_id="t1",
            context_id="c1",
            artifact=Artifact(parts=[TextPart(text="chunk")]),
            last_chunk=True,
        )
        sse = evt.to_sse()
        assert "event: artifact" in sse

    def test_push_notification_config(self):
        from cognithor.a2a.types import PushNotificationAuth, PushNotificationConfig

        auth = PushNotificationAuth(type="bearer", credentials="tok")
        config = PushNotificationConfig(
            task_id="t1",
            url="http://hook.url",
            authentication=auth,
            metadata={"x": 1},
        )
        d = config.to_dict()
        assert d["taskId"] == "t1"
        assert d["authentication"]["credentials"] == "tok"
        assert d["metadata"]["x"] == 1

    def test_agent_card_to_dict_full(self):
        from cognithor.a2a.types import (
            A2AAgentCapabilities,
            A2AAgentCard,
            A2AInterface,
            A2AProvider,
            A2ASecurityScheme,
            A2ASkill,
        )

        card = A2AAgentCard(
            name="TestAgent",
            url="http://agent.local",
            provider=A2AProvider(organization="TestOrg", url="http://org.local"),
            capabilities=A2AAgentCapabilities(streaming=True, push_notifications=True),
            skills=[
                A2ASkill(
                    id="s1",
                    name="Search",
                    description="Search web",
                    tags=["web"],
                    examples=["search for..."],
                )
            ],
            interfaces=[A2AInterface(url="http://agent.local/a2a")],
            security_schemes=[A2ASecurityScheme(description="Bearer token")],
            tags=["test"],
        )
        d = card.to_dict()
        assert d["name"] == "TestAgent"
        assert d["url"] == "http://agent.local"
        assert "skills" in d
        assert "interfaces" in d
        assert "securitySchemes" in d
        assert "tags" in d
        assert d["provider"]["url"] == "http://org.local"

    def test_agent_card_from_dict(self):
        from cognithor.a2a.types import A2AAgentCard

        data = {
            "name": "Test",
            "description": "A test agent",
            "version": "1.0",
            "provider": {"organization": "TestOrg"},
            "capabilities": {"streaming": True},
            "skills": [{"id": "s1", "name": "Search"}],
            "interfaces": [{"url": "http://x.com"}],
        }
        card = A2AAgentCard.from_dict(data)
        assert card.name == "Test"
        assert card.capabilities.streaming is True
        assert len(card.skills) == 1

    def test_a2a_skill_to_dict(self):
        from cognithor.a2a.types import A2ASkill

        s = A2ASkill(id="s1", name="Test", tags=["a"], examples=["ex1"])
        d = s.to_dict()
        assert d["tags"] == ["a"]
        assert d["examples"] == ["ex1"]
        # No tags/examples
        s2 = A2ASkill(id="s2", name="Plain")
        d2 = s2.to_dict()
        assert "tags" not in d2
        assert "examples" not in d2

    def test_a2a_security_scheme_to_dict(self):
        from cognithor.a2a.types import A2ASecurityScheme

        s = A2ASecurityScheme(description="Bearer auth")
        d = s.to_dict()
        assert d["description"] == "Bearer auth"
        s2 = A2ASecurityScheme()
        d2 = s2.to_dict()
        assert "description" not in d2

    def test_a2a_interface_to_dict(self):
        from cognithor.a2a.types import A2AInterface

        i = A2AInterface(url="http://x.com")
        d = i.to_dict()
        assert d["url"] == "http://x.com"

    def test_a2a_error_codes(self):
        from cognithor.a2a.types import A2AErrorCode

        assert A2AErrorCode.PARSE_ERROR == -32700
        assert A2AErrorCode.TASK_NOT_FOUND == -32001


# ============================================================================
# Browser Page Analyzer -- element extraction, fuzzy match
# ============================================================================


class TestPageAnalyzerDeep:
    async def test_analyze_full(self):
        from cognithor.browser.page_analyzer import PageAnalyzer

        pa = PageAnalyzer()
        page = MagicMock()
        page.url = "http://test.com"
        page.title = AsyncMock(return_value="Test Page")
        # Mock evaluate for various JS calls
        page.evaluate = AsyncMock(
            side_effect=[
                "Page text content",  # text extraction
                1500,  # html_length
                [
                    {
                        "selector": "a",
                        "text": "Link1",
                        "href": "/p1",
                        "visible": True,
                        "ariaLabel": "",
                    }
                ],
                [
                    {
                        "selector": "button",
                        "text": "Click",
                        "visible": True,
                        "enabled": True,
                        "ariaLabel": "",
                    }
                ],
                [
                    {
                        "selector": "input",
                        "type": "text",
                        "name": "user",
                        "value": "",
                        "placeholder": "Name",
                        "visible": True,
                        "enabled": True,
                        "required": True,
                        "ariaLabel": "",
                    }
                ],
                [
                    {
                        "selector": "form",
                        "action": "/submit",
                        "method": "POST",
                        "fields": [
                            {
                                "name": "user",
                                "type": "text",
                                "label": "Username",
                                "selector": "#user",
                            }
                        ],
                        "submitSelector": "button[type=submit]",
                        "name": "login",
                    }
                ],
                [{"headers": ["Col1"], "rows": [["val1"]]}],
            ]
        )
        state = await pa.analyze(page)
        assert state.is_loaded is True
        assert state.url == "http://test.com"
        assert state.title == "Test Page"
        assert len(state.links) >= 1
        assert len(state.buttons) >= 1
        assert len(state.inputs) >= 1
        assert len(state.forms) >= 1
        assert pa.stats()["analysis_count"] == 1

    async def test_analyze_error_path(self):
        """When page.url raises, the outer except catches it."""
        from cognithor.browser.page_analyzer import PageAnalyzer

        pa = PageAnalyzer()
        page = MagicMock()
        # Make url a property that raises
        type(page).url = property(lambda self: (_ for _ in ()).throw(Exception("no url")))
        page.title = AsyncMock(side_effect=Exception("no title"))
        state = await pa.analyze(page)
        assert len(state.errors) > 0

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

        pa = PageAnalyzer()
        page = MagicMock()
        page.evaluate = AsyncMock(return_value={"found": True, "selector": "#accept"})
        result = await pa.detect_cookie_banner(page)
        assert result["found"] is True

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

        pa = PageAnalyzer()
        page = MagicMock()
        page.evaluate = AsyncMock(side_effect=Exception("JS error"))
        result = await pa.detect_cookie_banner(page)
        assert result["found"] is False

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

        pa = PageAnalyzer()
        page = MagicMock()
        page.evaluate = AsyncMock(
            side_effect=[
                [
                    {
                        "selector": "button",
                        "text": "Login",
                        "visible": True,
                        "enabled": True,
                        "ariaLabel": "",
                    }
                ],
                [],  # links
                [],  # inputs
            ]
        )
        elem = await pa.find_element(page, "Login")
        assert elem is not None
        assert elem.text == "Login"

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

        pa = PageAnalyzer()
        page = MagicMock()
        page.evaluate = AsyncMock(
            side_effect=[
                [],  # buttons
                [
                    {
                        "selector": "a",
                        "text": "About Us",
                        "href": "/about",
                        "visible": True,
                        "ariaLabel": "",
                    }
                ],
                [],  # inputs
            ]
        )
        elem = await pa.find_element(page, "about")
        assert elem is not None

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

        pa = PageAnalyzer()
        page = MagicMock()
        page.evaluate = AsyncMock(
            side_effect=[
                [],  # buttons
                [],  # links
                [
                    {
                        "selector": "input",
                        "type": "email",
                        "name": "email",
                        "value": "",
                        "placeholder": "Enter email",
                        "visible": True,
                        "enabled": True,
                        "required": False,
                        "ariaLabel": "",
                    }
                ],
            ]
        )
        elem = await pa.find_element(page, "email")
        assert elem is not None

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

        pa = PageAnalyzer()
        page = MagicMock()
        page.evaluate = AsyncMock(return_value=[])
        elem = await pa.find_element(page, "nonexistent element xyz")
        assert elem is None

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

        pa = PageAnalyzer()
        assert pa._fuzzy_match("login", "Login Button") is True
        assert pa._fuzzy_match("login button", "Login") is True  # reverse containment
        assert pa._fuzzy_match("xyz", "Login") is False
        assert pa._fuzzy_match("test", "") is False

    async def test_extract_input_types(self):
        """Test different input type mappings in _extract_inputs."""
        from cognithor.browser.page_analyzer import PageAnalyzer

        pa = PageAnalyzer()
        page = MagicMock()
        page.evaluate = AsyncMock(
            return_value=[
                {
                    "selector": "#cb",
                    "type": "checkbox",
                    "name": "agree",
                    "value": "",
                    "placeholder": "",
                    "visible": True,
                    "enabled": True,
                    "required": False,
                    "ariaLabel": "",
                },
                {
                    "selector": "#rd",
                    "type": "radio",
                    "name": "option",
                    "value": "a",
                    "placeholder": "",
                    "visible": True,
                    "enabled": True,
                    "required": False,
                    "ariaLabel": "",
                },
                {
                    "selector": "#fl",
                    "type": "file",
                    "name": "upload",
                    "value": "",
                    "placeholder": "",
                    "visible": True,
                    "enabled": True,
                    "required": False,
                    "ariaLabel": "",
                },
                {
                    "selector": "#sl",
                    "type": "select",
                    "name": "country",
                    "value": "",
                    "placeholder": "",
                    "visible": True,
                    "enabled": True,
                    "required": False,
                    "ariaLabel": "",
                },
                {
                    "selector": "#ta",
                    "type": "textarea",
                    "name": "bio",
                    "value": "",
                    "placeholder": "",
                    "visible": True,
                    "enabled": True,
                    "required": False,
                    "ariaLabel": "",
                },
            ]
        )
        inputs = await pa._extract_inputs(page)
        types = [i.element_type.value for i in inputs]
        assert "checkbox" in types
        assert "radio" in types
        assert "file_input" in types
        assert "select" in types
        assert "textarea" in types

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

        pa = PageAnalyzer()
        page = MagicMock()
        page.evaluate = AsyncMock(
            return_value=[
                {
                    "selector": "form",
                    "action": "/login",
                    "method": "POST",
                    "name": "login_form",
                    "submitSelector": "button[type=submit]",
                    "fields": [
                        {
                            "name": "user",
                            "type": "text",
                            "label": "Username",
                            "value": "",
                            "placeholder": "User",
                            "required": True,
                            "options": [],
                            "selector": "#user",
                        },
                    ],
                },
            ]
        )
        forms = await pa._extract_forms(page)
        assert len(forms) == 1
        assert forms[0].action == "/login"
        assert len(forms[0].fields) == 1

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

        pa = PageAnalyzer()
        page = MagicMock()
        page.evaluate = AsyncMock(return_value=[{"headers": ["A", "B"], "rows": [["1", "2"]]}])
        tables = await pa._extract_tables(page)
        assert len(tables) == 1

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

        pa = PageAnalyzer()
        page = MagicMock()
        page.evaluate = AsyncMock(side_effect=Exception("JS error"))
        links = await pa._extract_links(page)
        assert links == []

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

        pa = PageAnalyzer()
        page = MagicMock()
        page.evaluate = AsyncMock(side_effect=Exception("JS error"))
        buttons = await pa._extract_buttons(page)
        assert buttons == []


# ============================================================================
# Browser Tools -- remaining vision tool coverage
# ============================================================================


class TestBrowserToolsVision:
    def _make_mock_mcp(self):
        mcp = MagicMock()
        mcp._tools = {}

        def register_builtin_handler(tool_name="", description="", input_schema=None, handler=None):
            mcp._tools[tool_name] = handler

        mcp.register_builtin_handler = register_builtin_handler
        return mcp

    async def test_vision_analyze_success(self):
        from cognithor.browser.tools import register_browser_use_tools

        mcp = self._make_mock_mcp()
        with patch("cognithor.browser.tools.BrowserAgent") as MockAgent:
            agent = MagicMock()
            agent.is_running = True
            vision = MagicMock()
            vision.is_enabled = True
            agent._vision = vision
            agent.analyze_page_with_vision = AsyncMock(return_value={"description": "A page"})
            MockAgent.return_value = agent
            register_browser_use_tools(mcp, vision_analyzer=MagicMock())
            result = json.loads(await mcp._tools["browser_vision_analyze"]({"prompt": "describe"}))
            assert "description" in result

    async def test_vision_find_success(self):
        from cognithor.browser.tools import register_browser_use_tools

        mcp = self._make_mock_mcp()
        with patch("cognithor.browser.tools.BrowserAgent") as MockAgent:
            agent = MagicMock()
            agent.is_running = True
            vision = MagicMock()
            vision.is_enabled = True
            agent._vision = vision
            res = MagicMock()
            res.to_dict.return_value = {"success": True, "clicked": True}
            agent.find_and_click_with_vision = AsyncMock(return_value=res)
            MockAgent.return_value = agent
            register_browser_use_tools(mcp, vision_analyzer=MagicMock())
            result = json.loads(
                await mcp._tools["browser_vision_find"]({"description": "blue button"})
            )
            assert result["success"] is True

    async def test_vision_screenshot_success_with_vision(self):
        from cognithor.browser.tools import register_browser_use_tools

        mcp = self._make_mock_mcp()
        with patch("cognithor.browser.tools.BrowserAgent") as MockAgent:
            agent = MagicMock()
            agent.is_running = True
            vision = MagicMock()
            vision.is_enabled = True
            vision_result = MagicMock()
            vision_result.success = True
            vision_result.description = "A login page"
            vision.analyze_screenshot = AsyncMock(return_value=vision_result)
            agent._vision = vision
            screenshot_res = MagicMock()
            screenshot_res.success = True
            screenshot_res.screenshot_b64 = "AAAA" * 100
            screenshot_res.to_dict.return_value = {"success": True}
            agent.screenshot = AsyncMock(return_value=screenshot_res)
            agent._extract_page_content = AsyncMock(return_value="page text")
            MockAgent.return_value = agent
            register_browser_use_tools(mcp, vision_analyzer=MagicMock())
            result = json.loads(await mcp._tools["browser_vision_screenshot"]({"full_page": False}))
            assert result["success"] is True
            assert result["description"] == "A login page"

    async def test_vision_screenshot_failure(self):
        from cognithor.browser.tools import register_browser_use_tools

        mcp = self._make_mock_mcp()
        with patch("cognithor.browser.tools.BrowserAgent") as MockAgent:
            agent = MagicMock()
            agent.is_running = True
            res = MagicMock()
            res.success = False
            res.to_dict.return_value = {"success": False, "error": "No page"}
            agent.screenshot = AsyncMock(return_value=res)
            MockAgent.return_value = agent
            register_browser_use_tools(mcp, vision_analyzer=MagicMock())
            result = json.loads(await mcp._tools["browser_vision_screenshot"]({}))
            assert result["success"] is False

    async def test_vision_screenshot_no_vision(self):
        from cognithor.browser.tools import register_browser_use_tools

        mcp = self._make_mock_mcp()
        with patch("cognithor.browser.tools.BrowserAgent") as MockAgent:
            agent = MagicMock()
            agent.is_running = True
            agent._vision = None
            res = MagicMock()
            res.success = True
            res.screenshot_b64 = "BBBB" * 50
            res.to_dict.return_value = {"success": True}
            agent.screenshot = AsyncMock(return_value=res)
            MockAgent.return_value = agent
            register_browser_use_tools(mcp, vision_analyzer=MagicMock())
            result = json.loads(await mcp._tools["browser_vision_screenshot"]({}))
            assert result["success"] is True
            assert "Vision nicht" in result["description"]


# ============================================================================
# Security: Audit -- record with AuditEntry, query with filters
# ============================================================================


class TestAuditRecord:
    def test_record_with_audit_entry(self, tmp_path):
        from cognithor.models import AuditEntry, GateStatus, RiskLevel
        from cognithor.security.audit import AuditTrail

        trail = AuditTrail(log_dir=tmp_path)
        entry = AuditEntry(
            session_id="s1",
            action_tool="read_file",
            action_params_hash="abc123",
            decision_status=GateStatus.ALLOW,
            decision_reason="Allowed by policy",
            risk_level=RiskLevel.GREEN,
            policy_name="default",
            execution_result="File content: ...",
        )
        h = trail.record(entry)
        assert h != ""
        assert trail.entry_count == 1

    def test_record_with_masking(self, tmp_path):
        from cognithor.models import AuditEntry, GateStatus
        from cognithor.security.audit import AuditTrail

        trail = AuditTrail(log_dir=tmp_path)
        entry = AuditEntry(
            session_id="s1",
            action_tool="api_call",
            action_params_hash="def456",
            decision_status=GateStatus.MASK,
            execution_result="Bearer eyJhbGciOiJIUzI1NiJ9.secret_data",
        )
        h = trail.record(entry, mask=True)
        assert h != ""

    def test_record_no_mask(self, tmp_path):
        from cognithor.models import AuditEntry, GateStatus
        from cognithor.security.audit import AuditTrail

        trail = AuditTrail(log_dir=tmp_path)
        entry = AuditEntry(
            session_id="s1",
            action_tool="test",
            action_params_hash="xxx",
            decision_status=GateStatus.ALLOW,
            error="Something went wrong",
        )
        h = trail.record(entry, mask=False)
        assert h != ""

    def test_query_with_session_filter(self, tmp_path):
        from cognithor.models import AuditEntry, GateStatus
        from cognithor.security.audit import AuditTrail

        trail = AuditTrail(log_dir=tmp_path)
        trail.record(
            AuditEntry(
                session_id="s1",
                action_tool="read_file",
                action_params_hash="a",
                decision_status=GateStatus.ALLOW,
            )
        )
        trail.record(
            AuditEntry(
                session_id="s2",
                action_tool="write_file",
                action_params_hash="b",
                decision_status=GateStatus.BLOCK,
            )
        )
        results = trail.query(session_id="s1")
        assert len(results) == 1
        assert results[0]["session_id"] == "s1"

    def test_query_with_tool_filter(self, tmp_path):
        from cognithor.models import AuditEntry, GateStatus
        from cognithor.security.audit import AuditTrail

        trail = AuditTrail(log_dir=tmp_path)
        trail.record(
            AuditEntry(
                session_id="s1",
                action_tool="read_file",
                action_params_hash="a",
                decision_status=GateStatus.ALLOW,
            )
        )
        trail.record(
            AuditEntry(
                session_id="s1",
                action_tool="exec_command",
                action_params_hash="b",
                decision_status=GateStatus.BLOCK,
            )
        )
        results = trail.query(tool="exec_command")
        assert len(results) == 1

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

        trail = AuditTrail(log_dir=tmp_path)
        for i in range(5):
            trail.record_event(f"s{i}", f"event_{i}")
        results = trail.query(limit=3)
        assert len(results) == 3

    def test_query_with_since_filter(self, tmp_path):
        from datetime import datetime

        from cognithor.security.audit import AuditTrail

        trail = AuditTrail(log_dir=tmp_path)
        trail.record_event("s1", "old_event")
        # All events will be after 2020
        since = datetime(2020, 1, 1, tzinfo=UTC)
        results = trail.query(since=since)
        assert len(results) >= 1


# ============================================================================
# Security: Agent Vault -- remaining classes
# ============================================================================


class TestAgentVaultSessions:
    def test_isolated_session_store_create_and_list(self):
        from cognithor.security.agent_vault import IsolatedSessionStore

        store = IsolatedSessionStore()
        sess = store.create_session("agent1", tenant_id="t1")
        assert sess.agent_id == "agent1"
        assert sess.is_active is True
        sessions = store.agent_sessions("agent1")
        assert len(sessions) == 1
        assert sessions[0].session_id == sess.session_id

    def test_isolated_session_store_get_session(self):
        from cognithor.security.agent_vault import IsolatedSessionStore

        store = IsolatedSessionStore()
        sess = store.create_session("agent1")
        found = store.get_session("agent1", sess.session_id)
        assert found is not None
        assert found.session_id == sess.session_id
        # Non-existent agent
        assert store.get_session("no-agent", sess.session_id) is None

    def test_isolated_session_store_close_and_active(self):
        from cognithor.security.agent_vault import IsolatedSessionStore

        store = IsolatedSessionStore()
        s1 = store.create_session("agent1")
        store.create_session("agent1")
        assert len(store.active_sessions("agent1")) == 2
        store.close_session("agent1", s1.session_id)
        assert len(store.active_sessions("agent1")) == 1
        # Close non-existent session
        assert store.close_session("agent1", "SESS-fake-0099") is False
        # Close for non-existent agent
        assert store.close_session("no-agent", s1.session_id) is False

    def test_isolated_session_store_destroy_and_purge(self):
        from cognithor.security.agent_vault import IsolatedSessionStore

        store = IsolatedSessionStore()
        s1 = store.create_session("agent1")
        store.create_session("agent1")
        assert store.total_sessions == 2
        assert store.store_count == 1
        store.destroy_session("agent1", s1.session_id)
        assert store.total_sessions == 1
        # Destroy non-existent
        assert store.destroy_session("agent1", "SESS-fake-0099") is False
        assert store.destroy_session("no-agent", "fake") is False
        # Purge
        count = store.purge_agent("agent1")
        assert count == 1
        assert store.total_sessions == 0

    def test_isolated_session_store_stats(self):
        from cognithor.security.agent_vault import IsolatedSessionStore

        store = IsolatedSessionStore()
        store.create_session("a1")
        store.create_session("a2")
        s3 = store.create_session("a1")
        store.close_session("a1", s3.session_id)
        st = store.stats()
        assert st["agent_stores"] == 2
        assert st["total_sessions"] == 3
        assert st["active_sessions"] == 2

    def test_session_firewall_authorize(self):
        from cognithor.security.agent_vault import IsolatedSessionStore, SessionFirewall

        store = IsolatedSessionStore()
        sess = store.create_session("agent1")
        fw = SessionFirewall(store)
        # Agent accessing own session should be allowed
        assert fw.authorize("agent1", "agent1", sess.session_id) is True
        assert fw.violation_count == 0
        # Cross-agent access should be blocked
        assert fw.authorize("agent2", "agent1", sess.session_id) is False
        assert fw.violation_count == 1
        violations = fw.violations()
        assert len(violations) == 1
        assert violations[0]["action"] == "BLOCKED"
        st = fw.stats()
        assert st["total_violations"] == 1
        assert st["unique_attackers"] == 1

    def test_vault_rotator_add_policy(self):
        from cognithor.security.agent_vault import RotationPolicy, SecretType, VaultRotator

        rotator = VaultRotator(load_defaults=False)
        assert rotator.policy_count == 0
        policy = RotationPolicy("custom", SecretType.API_KEY, 48, 720)
        rotator.add_policy(policy)
        assert "custom" in rotator._policies
        assert rotator.policy_count == 1
        # get_policy
        found = rotator.get_policy(SecretType.API_KEY)
        assert found is not None
        assert found.policy_id == "custom"

    def test_vault_rotator_defaults(self):
        from cognithor.security.agent_vault import VaultRotator

        rotator = VaultRotator(load_defaults=True)
        assert rotator.policy_count == 4
        st = rotator.stats()
        assert st["policies"] == 4
        assert st["total_rotations"] == 0
        assert len(st["policies_list"]) == 4

    def test_vault_rotator_check_and_auto_rotate(self):
        from cognithor.security.agent_vault import (
            AgentVault,
            RotationPolicy,
            SecretType,
            VaultRotator,
        )

        rotator = VaultRotator(load_defaults=False)
        # Add an auto-rotate policy for tokens with very short interval
        rotator.add_policy(RotationPolicy("ROT-TOK", SecretType.TOKEN, 0, 168, True, 4))
        vault = AgentVault("agent-test")
        sec = vault.store("mytoken", "old-value", SecretType.TOKEN)
        # Force the created_at to far in the past so rotation triggers
        sec.created_at = "2020-01-01T00:00:00Z"
        sec.last_rotated = ""
        needs = rotator.check_rotation_needed(vault)
        assert len(needs) >= 1
        rotated = rotator.auto_rotate(vault)
        assert len(rotated) >= 1


# ============================================================================
# Security: CICD Gate -- ContinuousRedTeam, DeploymentBlocker
# ============================================================================


class TestCICDGateMore:
    def test_continuous_red_team_run_probes(self):
        from cognithor.security.cicd_gate import ContinuousRedTeam

        crt = ContinuousRedTeam()
        assert crt.probe_count == 0
        assert crt.detection_rate() == 100.0  # no probes yet

        # handler_fn simulates a model response, is_blocked_fn checks if blocked
        def handler_fn(payload):
            return {"response": "I cannot do that.", "blocked": True}

        def is_blocked_fn(response):
            return response.get("blocked", False)

        result = crt.run_probes(handler_fn, is_blocked_fn, categories=["prompt_injection"])
        assert result["total_probes"] == 5  # 5 prompts in prompt_injection category
        assert result["overall_pass_rate"] == 100.0  # all blocked
        assert "prompt_injection" in result["categories"]
        assert crt.probe_count == 5
        assert crt.detection_rate() == 100.0

    def test_continuous_red_team_partial_block(self):
        from cognithor.security.cicd_gate import ContinuousRedTeam

        crt = ContinuousRedTeam()
        call_count = {"n": 0}

        def handler_fn(payload):
            call_count["n"] += 1
            return {"response": "ok", "blocked": call_count["n"] % 2 == 0}

        def is_blocked_fn(response):
            return response.get("blocked", False)

        result = crt.run_probes(handler_fn, is_blocked_fn, categories=["escalation"])
        assert result["total_probes"] == 4  # 4 prompts in escalation category
        assert result["overall_pass_rate"] == 50.0  # 2 out of 4 blocked
        st = crt.stats()
        assert st["total_probes"] == 4
        assert "escalation" in st["by_category"]

    def test_continuous_red_team_handler_exception(self):
        """When handler raises, the probe should be marked as blocked."""
        from cognithor.security.cicd_gate import ContinuousRedTeam

        crt = ContinuousRedTeam()

        def handler_fn(payload):
            raise RuntimeError("Simulated crash")

        def is_blocked_fn(response):
            return False

        result = crt.run_probes(handler_fn, is_blocked_fn, categories=["exfiltration"])
        # All should be blocked because handler raised
        assert result["overall_pass_rate"] == 100.0

    def test_red_team_probe_to_dict(self):
        from cognithor.security.cicd_gate import RedTeamProbe

        probe = RedTeamProbe(
            probe_id="RT-00001",
            category="prompt_injection",
            payload="Ignore all rules",
            blocked=True,
            timestamp="2026-03-02T00:00:00Z",
            latency_ms=12.345,
        )
        d = probe.to_dict()
        assert d["probe_id"] == "RT-00001"
        assert d["blocked"] is True
        assert d["latency_ms"] == 12.3

    def test_webhook_notifier(self):
        from cognithor.security.cicd_gate import WebhookConfig, WebhookNotifier

        notifier = WebhookNotifier()
        wh1 = WebhookConfig("wh1", "https://hooks.example.com/1", events=["gate_fail"])
        wh2 = WebhookConfig("wh2", "https://hooks.example.com/2", events=["*"], enabled=False)
        notifier.register(wh1)
        notifier.register(wh2)
        assert notifier.webhook_count == 2
        # Send notification -- wh1 matches, wh2 is disabled
        sent = notifier.notify("gate_fail", {"pipeline": "CI-1"})
        assert sent == 1
        assert notifier.sent_count == 1
        # Unmatched event
        sent2 = notifier.notify("unknown_event", {"x": 1})
        assert sent2 == 0
        st = notifier.stats()
        assert st["webhooks"] == 2
        assert st["enabled"] == 1
        assert st["notifications_sent"] == 1

    def test_webhook_config_to_dict(self):
        from cognithor.security.cicd_gate import WebhookConfig

        wh = WebhookConfig("wh1", "https://example.com", events=["gate_fail", "critical_finding"])
        d = wh.to_dict()
        assert d["webhook_id"] == "wh1"
        assert d["enabled"] is True

    def test_scan_schedule_and_scheduler(self):
        from cognithor.security.cicd_gate import ScanSchedule, ScanScheduler

        # Create a custom schedule
        scan = ScanSchedule(
            schedule_id="SC-1",
            name="Custom Scan",
            cron_expression="0 2 * * *",
            categories=["prompt_injection", "jailbreak"],
        )
        d = scan.to_dict()
        assert d["schedule_id"] == "SC-1"
        assert d["cron"] == "0 2 * * *"

        # Test ScanScheduler with defaults
        scheduler = ScanScheduler()
        assert scheduler.schedule_count == 3  # 3 default schedules
        enabled = scheduler.enabled_schedules()
        assert len(enabled) == 3

        # Add and remove
        scheduler.add(scan)
        assert scheduler.schedule_count == 4
        found = scheduler.get("SC-1")
        assert found is not None
        assert found.name == "Custom Scan"
        assert scheduler.get("nonexistent") is None
        removed = scheduler.remove("SC-1")
        assert removed is True
        assert scheduler.schedule_count == 3
        assert scheduler.remove("nonexistent") is False

        # Stats
        st = scheduler.stats()
        assert st["total_schedules"] == 3
        assert "schedules" in st

    def test_scan_scheduler_empty(self):
        from cognithor.security.cicd_gate import ScanScheduler

        scheduler = ScanScheduler(schedules=[])
        assert scheduler.schedule_count == 0
        assert scheduler.enabled_schedules() == []
