"""End-to-end security integration tests.

Cycle 23/25: Tests the full security pipeline from ingest through
scanning, policy evaluation, rate limiting, and audit logging.
Exercises the same code paths as the MCP plugin server.py.
"""
import os
import tempfile
import time
from pathlib import Path

import pytest

from greymatter_core.scanner import SecretScanner, ScannerConfig

_has_yaml = True
try:
    from greymatter_core.policy import (
        PolicyEngine,
        PolicyRule,
        PolicyOutcome,
        Condition,
        EvalContext,
        PolicyDecision,
    )
except ImportError:
    _has_yaml = False


_has_sqlite_vec = True
try:
    import sqlite_vec  # noqa: F401
except ImportError:
    _has_sqlite_vec = False


@pytest.fixture
def db(tmp_path):
    from greymatter_core.db import SQLiteDatabase
    return SQLiteDatabase(str(tmp_path / "e2e.db"))


@pytest.fixture
def scanner():
    return SecretScanner(ScannerConfig(mode="redact"))


@pytest.fixture
def block_scanner():
    return SecretScanner(ScannerConfig(mode="block"))


@pytest.fixture
def policy_engine():
    if not _has_yaml:
        pytest.skip("pyyaml not installed")
    rules = [
        PolicyRule(
            rule_id="block-secrets-tool",
            description="Block direct secret storage",
            outcome=PolicyOutcome.REFUSE,
            priority=10,
            conditions=[
                Condition(field="tool", operator="equals", value="gm_observe"),
                Condition(field="content", operator="matches", value=r"AKIA[0-9A-Z]{16}"),
            ],
            reason="Content contains AWS credentials",
        ),
        PolicyRule(
            rule_id="shadow-large-content",
            description="Shadow log large content for review",
            outcome=PolicyOutcome.ESCALATE,
            priority=50,
            shadow=True,
            conditions=[
                Condition(field="content", operator="greater_than", value="50000"),
            ],
            reason="Content exceeds 50KB",
        ),
    ]
    return PolicyEngine(rules)


# =========================================================================
# Full pipeline: ingest → scan → policy → audit
# =========================================================================


@pytest.mark.skipif(not _has_sqlite_vec, reason="sqlite_vec not installed")
class TestIngestPipeline:

    def test_clean_content_passes_all_checks(self, db, scanner, policy_engine):
        """Clean content passes scanner + policy without modification."""
        content = "Deployed service to production at 14:32 UTC. All health checks green."

        # Scanner
        detections = scanner.scan(content)
        assert len(detections) == 0

        # Policy
        ctx = EvalContext(tool="gm_observe", action="create", content=content)
        decision = policy_engine.evaluate(ctx)
        assert decision.outcome == PolicyOutcome.ALLOW

        # Store
        db._conn.execute(
            "INSERT INTO observations (id, session_id, trigger, title, content, category, source_tool) VALUES (?, 'e2e', 'test', 'e2e-test', ?, ?, ?)",
            ("obs-e2e-1", content, "deployment", "e2e-test"),
        )
        db._conn.commit()

        row = db._conn.execute(
            "SELECT content FROM observations WHERE id = ?", ("obs-e2e-1",)
        ).fetchone()
        assert row[0] == content

    def test_secret_content_redacted_on_ingest(self, db, scanner):
        """Content with secrets gets redacted before storage."""
        content = "AWS key is AKIAIOSFODNN7EXAMPLE and password=SuperSecret123!"

        detections = scanner.scan(content)
        assert len(detections) >= 2

        redacted = scanner.redact(content, detections)
        assert "AKIAIOSFODNN7EXAMPLE" not in redacted
        assert "SuperSecret123" not in redacted
        assert "[AWS_ACCESS_KEY_REDACTED]" in redacted

        # Store redacted version
        db._conn.execute(
            "INSERT INTO observations (id, session_id, trigger, title, content, category, source_tool) VALUES (?, 'e2e', 'test', 'e2e-test', ?, ?, ?)",
            ("obs-e2e-2", redacted, "security", "e2e-test"),
        )
        db._conn.commit()

        row = db._conn.execute(
            "SELECT content FROM observations WHERE id = ?", ("obs-e2e-2",)
        ).fetchone()
        assert "AKIAIOSFODNN7EXAMPLE" not in row[0]

    def test_secret_content_blocked_in_block_mode(self, block_scanner):
        """Block mode prevents storage entirely."""
        content = "key: AKIAIOSFODNN7EXAMPLE"
        detections = block_scanner.scan(content)
        assert len(detections) >= 1
        # In block mode, the caller should refuse to store
        assert block_scanner.config.mode == "block"

    def test_policy_refuses_credential_in_observe(self, policy_engine):
        """Policy engine blocks AWS credentials in gm_observe."""
        content = "Store this: AKIAIOSFODNN7EXAMPLE"
        ctx = EvalContext(tool="gm_observe", action="create", content=content)
        decision = policy_engine.evaluate(ctx)
        assert decision.outcome == PolicyOutcome.REFUSE
        assert "AWS credentials" in decision.reason

    def test_policy_allows_non_matching_tool(self, policy_engine):
        """Policy rule scoped to gm_observe doesn't affect gm_pattern."""
        content = "Store this: AKIAIOSFODNN7EXAMPLE"
        ctx = EvalContext(tool="gm_pattern", action="create", content=content)
        decision = policy_engine.evaluate(ctx)
        # The rule only matches tool=gm_observe, so gm_pattern is allowed
        assert decision.outcome == PolicyOutcome.ALLOW


# =========================================================================
# Audit log integration
# =========================================================================


@pytest.mark.skipif(not _has_sqlite_vec, reason="sqlite_vec not installed")
class TestAuditLogIntegration:

    def test_security_events_table_exists(self, db):
        """The security_events table is created during migration."""
        tables = db._conn.execute(
            "SELECT name FROM sqlite_master WHERE type='table' AND name='security_events'"
        ).fetchall()
        assert len(tables) == 1

    def test_log_and_retrieve_events(self, db):
        """Events can be logged and queried."""
        db._conn.execute(
            "INSERT INTO security_events (event_type, severity, pattern_name, "
            "tool_name, action_taken, detail) VALUES (?, ?, ?, ?, ?, ?)",
            ("ingest_redacted", "high", "aws_access_key", "gm_observe",
             "redacted", "Secret redacted on ingest"),
        )
        db._conn.commit()

        events = db._conn.execute(
            "SELECT event_type, severity, pattern_name, action_taken FROM security_events"
        ).fetchall()
        assert len(events) == 1
        assert events[0][0] == "ingest_redacted"
        assert events[0][1] == "high"
        assert events[0][2] == "aws_access_key"
        assert events[0][3] == "redacted"

    def test_audit_log_append_only(self, db):
        """IDs strictly increase — no gaps, no overwrites."""
        for i in range(10):
            db._conn.execute(
                "INSERT INTO security_events (event_type, severity, action_taken) "
                "VALUES (?, ?, ?)",
                (f"event_{i}", "low", "logged"),
            )
        db._conn.commit()

        ids = [
            r[0] for r in
            db._conn.execute("SELECT id FROM security_events ORDER BY id").fetchall()
        ]
        assert ids == list(range(1, 11))

    def test_events_have_timestamps(self, db):
        """created_at is auto-populated."""
        db._conn.execute(
            "INSERT INTO security_events (event_type, severity, action_taken) "
            "VALUES (?, ?, ?)",
            ("timestamp_test", "low", "logged"),
        )
        db._conn.commit()

        ts = db._conn.execute(
            "SELECT created_at FROM security_events WHERE event_type = 'timestamp_test'"
        ).fetchone()[0]
        assert ts is not None
        assert len(ts) >= 19  # datetime format


# =========================================================================
# Scanner + Policy + Audit: full chain
# =========================================================================


@pytest.mark.skipif(not _has_sqlite_vec, reason="sqlite_vec not installed")
class TestFullSecurityChain:

    def test_scanner_detects_then_policy_blocks_then_audit_logs(self, db, scanner, policy_engine):
        """Full chain: scan → detect → policy block → audit log."""
        content = "Deploy with AKIAIOSFODNN7EXAMPLE credentials"

        # 1. Scanner detects
        detections = scanner.scan(content)
        assert any(d.pattern_name == "aws_access_key" for d in detections)

        # 2. Policy blocks
        ctx = EvalContext(tool="gm_observe", action="create", content=content)
        decision = policy_engine.evaluate(ctx)
        assert decision.outcome == PolicyOutcome.REFUSE

        # 3. Audit log records
        db._conn.execute(
            "INSERT INTO security_events (event_type, severity, pattern_name, "
            "tool_name, action_taken, detail) VALUES (?, ?, ?, ?, ?, ?)",
            ("policy_refused", "high", "aws_access_key", "gm_observe",
             "refused", f"Rule {decision.rule_id}: {decision.reason}"),
        )
        db._conn.commit()

        event = db._conn.execute(
            "SELECT event_type, action_taken, detail FROM security_events "
            "WHERE event_type = 'policy_refused'"
        ).fetchone()
        assert event is not None
        assert "refused" in event[1]
        assert "block-secrets-tool" in event[2]

    def test_redact_then_store_then_recall_clean(self, db, scanner):
        """Content is redacted on ingest and stays clean on recall."""
        original = "My password=TopSecret99! and key AKIAIOSFODNN7EXAMPLE"
        redacted = scanner.redact(original)

        # Store redacted
        db._conn.execute(
            "INSERT INTO observations (id, session_id, trigger, title, content, category, source_tool) VALUES (?, 'e2e', 'test', 'e2e-test', ?, ?, ?)",
            ("obs-chain-1", redacted, "test", "e2e"),
        )
        db._conn.commit()

        # Recall
        recalled = db._conn.execute(
            "SELECT content FROM observations WHERE id = ?", ("obs-chain-1",)
        ).fetchone()[0]

        # Recalled text should have no secrets
        recall_detections = scanner.scan(recalled)
        secret_detections = [d for d in recall_detections if d.severity in ("critical", "high")]
        assert len(secret_detections) == 0, f"Recalled text still has secrets: {secret_detections}"


# =========================================================================
# Policy YAML round-trip
# =========================================================================


@pytest.mark.skipif(not _has_yaml, reason="pyyaml not installed")
class TestPolicyYAMLRoundTrip:

    def test_from_yaml_and_evaluate(self):
        """Load rules from YAML, evaluate, and verify."""
        yaml_text = """
rules:
  - rule_id: block-ssn
    description: Block SSN storage
    outcome: refuse
    priority: 10
    reason: Content contains SSN
    conditions:
      - field: content
        operator: matches
        value: "\\\\d{3}-\\\\d{2}-\\\\d{4}"
  - rule_id: allow-all
    description: Default allow
    outcome: allow
    priority: 999
"""
        engine = PolicyEngine.from_yaml(yaml_text)
        assert len(engine.rules) == 2

        # SSN should be blocked
        ctx = EvalContext(content="SSN: 123-45-6789")
        decision = engine.evaluate(ctx)
        assert decision.outcome == PolicyOutcome.REFUSE

        # Clean content should be allowed
        ctx = EvalContext(content="Normal text")
        decision = engine.evaluate(ctx)
        assert decision.outcome == PolicyOutcome.ALLOW

    def test_yaml_export_import_preserves_rules(self):
        """Export to YAML and re-import preserves all rule properties."""
        original = PolicyEngine([
            PolicyRule(
                rule_id="test-1",
                description="Test rule",
                outcome=PolicyOutcome.REDACT,
                priority=42,
                conditions=[Condition("content", "contains", "secret")],
                projects=["proj-a"],
                shadow=True,
                reason="Testing",
            ),
        ])

        yaml_text = original.to_yaml()
        restored = PolicyEngine.from_yaml(yaml_text)

        assert len(restored.rules) == 1
        r = restored.rules[0]
        assert r.rule_id == "test-1"
        assert r.outcome == PolicyOutcome.REDACT
        assert r.priority == 42
        assert r.shadow is True
        assert len(r.conditions) == 1
        assert r.conditions[0].value == "secret"
        assert r.projects == ["proj-a"]
