"""Tests for the compliance module."""

import json
import os
import tempfile
import time
import uuid

import pytest

from horizon.compliance import (
    AccessControl,
    ApprovalGateway,
    ApprovalRequest,
    ApprovalStatus,
    AuditEvent,
    AuditEventType,
    AuditTrail,
    ComplianceAction,
    ComplianceConfig,
    ComplianceKillSwitch,
    ComplianceManager,
    ComplianceReport,
    ComplianceStore,
    DailyTradeReport,
    PERMISSIONS,
    PositionReport,
    RegulatoryLimitConfig,
    RegulatoryLimits,
    ReportGenerator,
    RetentionManager,
    Role,
    SurveillanceAlert,
    SurveillanceAlertType,
    TradeSurveillance,
    compliance_gate,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _tmp_db():
    """Create a temporary compliance database."""
    fd, path = tempfile.mkstemp(suffix=".db")
    os.close(fd)
    return path


def _default_config(**overrides) -> ComplianceConfig:
    """Build a config with sensible test defaults."""
    defaults = dict(
        enabled=True,
        audit_enabled=True,
        surveillance_enabled=True,
        approval_enabled=False,
        roles={
            "admin": Role.ADMIN,
            "officer": Role.COMPLIANCE_OFFICER,
            "risk": Role.RISK_MANAGER,
            "trader": Role.TRADER,
            "viewer": Role.VIEWER,
        },
    )
    defaults.update(overrides)
    return ComplianceConfig(**defaults)


class FakePosition:
    def __init__(self, market_id, side, size, avg_entry_price, exchange="paper"):
        self.market_id = market_id
        self.side = side
        self.size = size
        self.avg_entry_price = avg_entry_price
        self.exchange = exchange


class FakeEngine:
    """Minimal engine mock for compliance tests."""

    def __init__(self, positions=None):
        self._positions = positions or []
        self._kill_switch_active = False
        self._kill_reason = None

    def positions(self):
        return list(self._positions)

    def positions_for_market(self, market_id):
        return [p for p in self._positions if p.market_id == market_id]

    def activate_kill_switch(self, reason):
        self._kill_switch_active = True
        self._kill_reason = reason

    def deactivate_kill_switch(self):
        self._kill_switch_active = False


# ===========================================================================
# ComplianceStore tests
# ===========================================================================


class TestComplianceStore:
    def setup_method(self):
        self.db_path = _tmp_db()
        self.store = ComplianceStore(self.db_path)

    def teardown_method(self):
        self.store.close()
        os.unlink(self.db_path)

    def test_insert_and_query_audit_event(self):
        event = AuditEvent(
            event_id="evt1", event_type=AuditEventType.ORDER_SUBMITTED,
            timestamp=1000.0, actor="trader1", details={"price": 0.55},
            market_id="m1", event_hash="abc123",
        )
        self.store.insert_audit_event(event)
        results = self.store.query_audit_events(start=999, end=1001)
        assert len(results) == 1
        assert results[0].event_id == "evt1"
        assert results[0].details == {"price": 0.55}

    def test_query_filter_by_type(self):
        for i, etype in enumerate([AuditEventType.ORDER_SUBMITTED, AuditEventType.ORDER_FILLED]):
            self.store.insert_audit_event(AuditEvent(
                event_id=f"evt{i}", event_type=etype,
                timestamp=1000.0 + i, actor="sys", details={}, event_hash=f"h{i}",
            ))
        results = self.store.query_audit_events(event_type="order_filled")
        assert len(results) == 1
        assert results[0].event_type == AuditEventType.ORDER_FILLED

    def test_insert_and_query_alert(self):
        alert = SurveillanceAlert(
            alert_id="a1", alert_type=SurveillanceAlertType.WASH_TRADING,
            severity="critical", timestamp=2000.0, market_id="m1",
            description="test", evidence={"key": "val"},
        )
        self.store.insert_alert(alert)
        results = self.store.query_alerts(start=1999)
        assert len(results) == 1
        assert results[0].alert_id == "a1"

    def test_resolve_alert(self):
        alert = SurveillanceAlert(
            alert_id="a2", alert_type=SurveillanceAlertType.SPOOFING,
            severity="warning", timestamp=2000.0, market_id="m1",
            description="spoof", evidence={},
        )
        self.store.insert_alert(alert)
        self.store.resolve_alert("a2", "officer", "false positive")
        results = self.store.query_alerts(resolved=True)
        assert len(results) == 1

    def test_insert_and_query_approval(self):
        req = ApprovalRequest(
            request_id="req1", order_data={"price": 0.6},
            market_id="m1", requester="trader1", reason="large",
            timestamp=3000.0, expires_at=3300.0,
        )
        self.store.insert_approval(req)
        pending = self.store.get_pending_approvals()
        assert len(pending) == 1
        assert pending[0].request_id == "req1"

    def test_update_approval_status(self):
        req = ApprovalRequest(
            request_id="req2", order_data={}, market_id="m1",
            requester="t", reason="r", timestamp=3000.0, expires_at=3300.0,
        )
        self.store.insert_approval(req)
        self.store.update_approval_status("req2", ApprovalStatus.APPROVED, "officer")
        got = self.store.get_approval("req2")
        assert got.status == ApprovalStatus.APPROVED

    def test_last_event_hash(self):
        assert self.store.last_event_hash() is None
        self.store.insert_audit_event(AuditEvent(
            event_id="e1", event_type=AuditEventType.CONFIG_CHANGED,
            timestamp=1.0, actor="sys", details={}, event_hash="hash1",
        ))
        assert self.store.last_event_hash() == "hash1"

    def test_event_count(self):
        assert self.store.event_count() == 0
        self.store.insert_audit_event(AuditEvent(
            event_id="e1", event_type=AuditEventType.CONFIG_CHANGED,
            timestamp=1.0, actor="sys", details={}, event_hash="h1",
        ))
        assert self.store.event_count() == 1

    def test_purge_before(self):
        for i in range(5):
            self.store.insert_audit_event(AuditEvent(
                event_id=f"e{i}", event_type=AuditEventType.ORDER_SUBMITTED,
                timestamp=float(i), actor="sys", details={}, event_hash=f"h{i}",
            ))
        purged = self.store.purge_before(3.0)
        assert purged == 3
        remaining = self.store.query_audit_events(limit=100)
        assert len(remaining) == 2

    def test_metadata(self):
        assert self.store.get_meta("version") is None
        self.store.set_meta("version", "1.0")
        assert self.store.get_meta("version") == "1.0"

    def test_alert_count(self):
        for i in range(3):
            self.store.insert_alert(SurveillanceAlert(
                alert_id=f"a{i}", alert_type=SurveillanceAlertType.RAPID_FIRE,
                severity="info", timestamp=100.0 + i, market_id="m",
                description="t", evidence={},
            ))
        assert self.store.alert_count(start=100, end=103) == 3
        assert self.store.alert_count(start=100.5, end=101.5) == 1


# ===========================================================================
# AuditTrail tests
# ===========================================================================


class TestAuditTrail:
    def setup_method(self):
        self.db_path = _tmp_db()
        self.store = ComplianceStore(self.db_path)
        self.audit = AuditTrail(self.store)

    def teardown_method(self):
        self.store.close()
        os.unlink(self.db_path)

    def test_record_creates_event(self):
        event = self.audit.record(AuditEventType.ORDER_SUBMITTED, "trader1", {"price": 0.5})
        assert event.event_type == AuditEventType.ORDER_SUBMITTED
        assert event.actor == "trader1"
        assert event.event_hash is not None
        assert len(event.event_hash) == 64  # SHA-256 hex

    def test_hash_chain_links(self):
        e1 = self.audit.record(AuditEventType.ORDER_SUBMITTED, "t", {})
        e2 = self.audit.record(AuditEventType.ORDER_FILLED, "t", {})
        assert e2.prev_hash == e1.event_hash

    def test_verify_integrity_valid(self):
        for i in range(10):
            self.audit.record(AuditEventType.ORDER_SUBMITTED, "t", {"i": i})
        valid, count = self.audit.verify_integrity()
        assert valid is True
        assert count == 10

    def test_verify_integrity_empty(self):
        valid, count = self.audit.verify_integrity()
        assert valid is True
        assert count == 0

    def test_query_with_filters(self):
        self.audit.record(AuditEventType.ORDER_SUBMITTED, "t", {}, market_id="m1")
        self.audit.record(AuditEventType.ORDER_FILLED, "t", {}, market_id="m2")
        results = self.audit.query(market_id="m1")
        assert len(results) == 1

    def test_export_csv(self):
        for i in range(5):
            self.audit.record(AuditEventType.ORDER_SUBMITTED, "t", {"i": i})
        with tempfile.NamedTemporaryFile(suffix=".csv", delete=False) as f:
            csv_path = f.name
        try:
            count = self.audit.export_csv(csv_path)
            assert count == 5
            with open(csv_path) as f:
                lines = f.readlines()
            assert len(lines) == 6  # header + 5 rows
        finally:
            os.unlink(csv_path)

    def test_count_property(self):
        assert self.audit.count == 0
        self.audit.record(AuditEventType.ORDER_SUBMITTED, "t", {})
        assert self.audit.count == 1

    def test_chain_resumes_from_persistence(self):
        """New AuditTrail instance resumes the hash chain."""
        e1 = self.audit.record(AuditEventType.ORDER_SUBMITTED, "t", {})
        audit2 = AuditTrail(self.store)
        e2 = audit2.record(AuditEventType.ORDER_FILLED, "t", {})
        assert e2.prev_hash == e1.event_hash


# ===========================================================================
# AccessControl tests
# ===========================================================================


class TestAccessControl:
    def setup_method(self):
        self.db_path = _tmp_db()
        self.store = ComplianceStore(self.db_path)
        self.audit = AuditTrail(self.store)
        self.config = _default_config()
        self.access = AccessControl(self.config, self.store, self.audit)

    def teardown_method(self):
        self.store.close()
        os.unlink(self.db_path)

    def test_get_role(self):
        assert self.access.get_role("admin") == Role.ADMIN
        assert self.access.get_role("unknown") is None

    def test_check_permission_granted(self):
        assert self.access.check_permission("admin", "purge_data") is True

    def test_check_permission_denied(self):
        assert self.access.check_permission("viewer", "submit_order") is False

    def test_require_permission_raises(self):
        with pytest.raises(PermissionError, match="lacks permission"):
            self.access.require_permission("viewer", "submit_order")

    def test_require_permission_passes(self):
        self.access.require_permission("trader", "submit_order")  # No error

    def test_assign_role(self):
        self.access.assign_role("new_user", Role.TRADER, "admin")
        assert self.access.get_role("new_user") == Role.TRADER

    def test_assign_role_requires_admin(self):
        with pytest.raises(PermissionError):
            self.access.assign_role("new_user", Role.TRADER, "trader")

    def test_revoke_role(self):
        self.access.revoke_role("trader", "admin")
        assert self.access.get_role("trader") is None

    def test_list_roles(self):
        roles = self.access.list_roles()
        assert "admin" in roles
        assert roles["admin"] == "admin"

    def test_permission_hierarchy(self):
        """ADMIN has all permissions."""
        all_perms = set()
        for perms in PERMISSIONS.values():
            all_perms.update(perms)
        for perm in all_perms:
            assert self.access.check_permission("admin", perm)


# ===========================================================================
# TradeSurveillance tests
# ===========================================================================


class TestTradeSurveillance:
    def setup_method(self):
        self.db_path = _tmp_db()
        self.store = ComplianceStore(self.db_path)
        self.audit = AuditTrail(self.store)
        self.config = _default_config(
            surveillance_window_secs=60.0,
            wash_trade_threshold_secs=5.0,
            spoof_cancel_threshold_secs=2.0,
            layering_depth=3,
        )
        self.surv = TradeSurveillance(self.config, self.store, self.audit)

    def teardown_method(self):
        self.store.close()
        os.unlink(self.db_path)

    def test_wash_trading_detection(self):
        now = time.time()
        self.surv.record_fill(now, "m1", "buy", 10.0)
        self.surv.record_fill(now + 1, "m1", "sell", 10.0)
        alerts = self.surv.analyze_cycle()
        wash_alerts = [a for a in alerts if a.alert_type == SurveillanceAlertType.WASH_TRADING]
        assert len(wash_alerts) >= 1
        assert wash_alerts[0].severity == "critical"

    def test_no_wash_trading_when_outside_threshold(self):
        now = time.time()
        self.surv.record_fill(now - 100, "m1", "buy", 10.0)
        self.surv.record_fill(now, "m1", "sell", 10.0)
        alerts = self.surv.analyze_cycle()
        wash_alerts = [a for a in alerts if a.alert_type == SurveillanceAlertType.WASH_TRADING]
        assert len(wash_alerts) == 0

    def test_spoofing_detection(self):
        now = time.time()
        self.surv.record_order(now, "m1", "buy", 100.0, "ord1")
        self.surv.record_cancel(now + 0.5, "ord1", "m1")
        alerts = self.surv.analyze_cycle()
        spoof_alerts = [a for a in alerts if a.alert_type == SurveillanceAlertType.SPOOFING]
        assert len(spoof_alerts) >= 1

    def test_layering_detection(self):
        now = time.time()
        for i in range(5):
            self.surv.record_order(now, "m1", "buy", 10.0, f"ord{i}")
        alerts = self.surv.analyze_cycle()
        layer_alerts = [a for a in alerts if a.alert_type == SurveillanceAlertType.LAYERING]
        assert len(layer_alerts) >= 1

    def test_rapid_fire_detection(self):
        config = _default_config(
            regulatory_limits=RegulatoryLimitConfig(max_orders_per_minute=5),
        )
        surv = TradeSurveillance(config, self.store, self.audit)
        now = time.time()
        for i in range(10):
            surv.record_order(now, "m1", "buy", 1.0, f"o{i}")
        alerts = surv.analyze_cycle()
        rapid = [a for a in alerts if a.alert_type == SurveillanceAlertType.RAPID_FIRE]
        assert len(rapid) >= 1

    def test_concentration_detection(self):
        engine = FakeEngine([
            FakePosition("m1", "buy", 90.0, 1.0),
            FakePosition("m2", "buy", 10.0, 1.0),
        ])
        config = _default_config(
            regulatory_limits=RegulatoryLimitConfig(max_concentration_pct=50.0),
        )
        surv = TradeSurveillance(config, self.store, self.audit)
        alerts = surv.analyze_cycle(engine)
        conc = [a for a in alerts if a.alert_type == SurveillanceAlertType.CONCENTRATION]
        assert len(conc) >= 1

    def test_no_alerts_when_disabled(self):
        config = _default_config(surveillance_enabled=False)
        surv = TradeSurveillance(config, self.store, self.audit)
        now = time.time()
        surv.record_fill(now, "m1", "buy", 10.0)
        surv.record_fill(now + 0.1, "m1", "sell", 10.0)
        assert surv.analyze_cycle() == []


# ===========================================================================
# ApprovalGateway tests
# ===========================================================================


class TestApprovalGateway:
    def setup_method(self):
        self.db_path = _tmp_db()
        self.store = ComplianceStore(self.db_path)
        self.audit = AuditTrail(self.store)
        self.config = _default_config(
            approval_enabled=True,
            approval_threshold_notional=100.0,
            approval_timeout_secs=300.0,
        )
        self.gw = ApprovalGateway(self.config, self.store, self.audit)

    def teardown_method(self):
        self.store.close()
        os.unlink(self.db_path)

    def test_auto_approve_below_threshold(self):
        result = self.gw.check_order({"price": 0.5, "size": 10}, "trader", "m1")
        assert result == ComplianceAction.APPROVED

    def test_pending_above_threshold(self):
        result = self.gw.check_order({"price": 0.5, "size": 500}, "trader", "m1")
        assert result == ComplianceAction.PENDING_APPROVAL

    def test_approve_pending_order(self):
        self.gw.check_order({"price": 0.5, "size": 500}, "trader", "m1")
        pending = self.gw.pending()
        assert len(pending) == 1
        ok = self.gw.approve(pending[0].request_id, "officer", "looks good")
        assert ok is True

    def test_deny_pending_order(self):
        self.gw.check_order({"price": 0.5, "size": 500}, "trader", "m1")
        pending = self.gw.pending()
        ok = self.gw.deny(pending[0].request_id, "officer", "too risky")
        assert ok is True

    def test_approve_nonexistent(self):
        assert self.gw.approve("fake_id", "officer") is False

    def test_deny_already_approved(self):
        self.gw.check_order({"price": 0.5, "size": 500}, "trader", "m1")
        pending = self.gw.pending()
        self.gw.approve(pending[0].request_id, "officer")
        assert self.gw.deny(pending[0].request_id, "officer") is False

    def test_expire_stale(self):
        config = _default_config(
            approval_enabled=True,
            approval_threshold_notional=100.0,
            approval_timeout_secs=0.0,  # immediate expiry
        )
        gw = ApprovalGateway(config, self.store, self.audit)
        gw.check_order({"price": 0.5, "size": 500}, "trader", "m1")
        expired = gw.expire_stale()
        assert expired == 1

    def test_drain_approved(self):
        self.gw.check_order({"price": 0.5, "size": 500, "bid": 0.4}, "trader", "m1")
        pending = self.gw.pending()
        self.gw.approve(pending[0].request_id, "officer")
        orders = self.gw.drain_approved()
        assert len(orders) == 1
        assert orders[0]["price"] == 0.5
        # Second drain should be empty
        assert self.gw.drain_approved() == []

    def test_disabled_auto_approves(self):
        config = _default_config(approval_enabled=False)
        gw = ApprovalGateway(config, self.store, self.audit)
        result = gw.check_order({"price": 0.5, "size": 9999}, "trader", "m1")
        assert result == ComplianceAction.APPROVED

    def test_callback_invoked(self):
        received = []
        config = _default_config(
            approval_enabled=True,
            approval_threshold_notional=100.0,
            approval_callback=lambda req: received.append(req),
        )
        gw = ApprovalGateway(config, self.store, self.audit)
        gw.check_order({"price": 0.5, "size": 500}, "trader", "m1")
        assert len(received) == 1
        assert isinstance(received[0], ApprovalRequest)


# ===========================================================================
# RegulatoryLimits tests
# ===========================================================================


class TestRegulatoryLimits:
    def setup_method(self):
        self.db_path = _tmp_db()
        self.store = ComplianceStore(self.db_path)
        self.audit = AuditTrail(self.store)

    def teardown_method(self):
        self.store.close()
        os.unlink(self.db_path)

    def test_restricted_market_rejected(self):
        config = RegulatoryLimitConfig(restricted_markets={"m1"})
        limits = RegulatoryLimits(config, self.store, self.audit)
        result = limits.check_order("m1", "buy", 0.5, 10, FakeEngine())
        assert result == ComplianceAction.REJECTED

    def test_allowed_market_approved(self):
        config = RegulatoryLimitConfig(restricted_markets={"m1"})
        limits = RegulatoryLimits(config, self.store, self.audit)
        result = limits.check_order("m2", "buy", 0.5, 10, FakeEngine())
        assert result == ComplianceAction.APPROVED

    def test_position_limit_rejected(self):
        config = RegulatoryLimitConfig(max_position_per_market={"m1": 100.0})
        engine = FakeEngine([FakePosition("m1", "buy", 95.0, 0.5)])
        limits = RegulatoryLimits(config, self.store, self.audit)
        result = limits.check_order("m1", "buy", 0.5, 10, engine)
        assert result == ComplianceAction.REJECTED

    def test_total_notional_rejected(self):
        config = RegulatoryLimitConfig(max_total_notional=1000.0)
        engine = FakeEngine([FakePosition("m1", "buy", 1000.0, 0.9)])
        limits = RegulatoryLimits(config, self.store, self.audit)
        result = limits.check_order("m2", "buy", 0.5, 500, engine)
        assert result == ComplianceAction.REJECTED

    def test_concentration_rejected(self):
        config = RegulatoryLimitConfig(max_concentration_pct=50.0)
        engine = FakeEngine([
            FakePosition("m1", "buy", 100.0, 1.0),
            FakePosition("m2", "buy", 10.0, 1.0),
        ])
        limits = RegulatoryLimits(config, self.store, self.audit)
        result = limits.check_order("m1", "buy", 1.0, 200, engine)
        assert result == ComplianceAction.REJECTED

    def test_daily_volume_rejected(self):
        config = RegulatoryLimitConfig(max_daily_volume=100.0)
        limits = RegulatoryLimits(config, self.store, self.audit)
        engine = FakeEngine()
        # First order: 50 notional -> approved
        r1 = limits.check_order("m1", "buy", 0.5, 100, engine)
        assert r1 == ComplianceAction.APPROVED
        # Second order: 60 notional -> total 110 > 100 -> rejected
        r2 = limits.check_order("m1", "buy", 0.6, 100, engine)
        assert r2 == ComplianceAction.REJECTED

    def test_current_utilization(self):
        config = RegulatoryLimitConfig(
            max_total_notional=1000.0,
            max_daily_volume=500.0,
        )
        engine = FakeEngine([FakePosition("m1", "buy", 100.0, 1.0)])
        limits = RegulatoryLimits(config, self.store, self.audit)
        util = limits.current_utilization(engine)
        assert "total_notional" in util
        assert util["total_notional"] == pytest.approx(10.0)


# ===========================================================================
# ComplianceKillSwitch tests
# ===========================================================================


class TestComplianceKillSwitch:
    def setup_method(self):
        self.db_path = _tmp_db()
        self.store = ComplianceStore(self.db_path)
        self.audit = AuditTrail(self.store)
        self.config = _default_config()
        self.access = AccessControl(self.config, self.store, self.audit)
        self.ks = ComplianceKillSwitch(self.audit, self.access)

    def teardown_method(self):
        self.store.close()
        os.unlink(self.db_path)

    def test_activate_requires_permission(self):
        self.ks.bind_engine(FakeEngine())
        with pytest.raises(PermissionError):
            self.ks.activate("test", "viewer")

    def test_activate_succeeds_for_risk_manager(self):
        engine = FakeEngine([FakePosition("m1", "buy", 10.0, 0.5)])
        self.ks.bind_engine(engine)
        self.ks.activate("emergency", "risk")
        assert self.ks.is_active is True
        assert engine._kill_switch_active is True

    def test_activate_without_engine_raises(self):
        with pytest.raises(RuntimeError, match="No engine bound"):
            self.ks.activate("test", "risk")

    def test_deactivate(self):
        engine = FakeEngine()
        self.ks.bind_engine(engine)
        self.ks.activate("test", "risk")
        self.ks.deactivate("risk", "situation resolved")
        assert self.ks.is_active is False

    def test_deactivate_requires_permission(self):
        engine = FakeEngine()
        self.ks.bind_engine(engine)
        self.ks.activate("test", "risk")
        with pytest.raises(PermissionError):
            self.ks.deactivate("viewer", "just because")

    def test_status(self):
        status = self.ks.status()
        assert status["active"] is False
        assert status["activated_by"] is None


# ===========================================================================
# ReportGenerator tests
# ===========================================================================


class TestReportGenerator:
    def setup_method(self):
        self.db_path = _tmp_db()
        self.store = ComplianceStore(self.db_path)
        self.audit = AuditTrail(self.store)
        self.reporting = ReportGenerator(self.store, self.audit)

    def teardown_method(self):
        self.store.close()
        os.unlink(self.db_path)

    def test_daily_trade_report_empty(self):
        report = self.reporting.daily_trade_report("2026-01-01")
        assert isinstance(report, DailyTradeReport)
        assert report.total_orders == 0
        assert report.date == "2026-01-01"

    def test_position_report(self):
        engine = FakeEngine([
            FakePosition("m1", "buy", 100.0, 0.5),
            FakePosition("m2", "buy", 50.0, 0.8),
        ])
        report = self.reporting.position_report(engine)
        assert isinstance(report, PositionReport)
        assert report.total_notional == 100 * 0.5 + 50 * 0.8
        assert len(report.positions) == 2
        assert "m1" in report.concentration
        assert "m2" in report.concentration

    def test_compliance_report(self):
        engine = FakeEngine()
        now = time.time()
        report = self.reporting.compliance_report(now - 86400, now, engine)
        assert isinstance(report, ComplianceReport)
        assert report.chain_integrity is True


# ===========================================================================
# RetentionManager tests
# ===========================================================================


class TestRetentionManager:
    def setup_method(self):
        self.db_path = _tmp_db()
        self.store = ComplianceStore(self.db_path)
        self.audit = AuditTrail(self.store)

    def teardown_method(self):
        self.store.close()
        os.unlink(self.db_path)

    def test_archive_and_purge(self):
        config = _default_config(retention_days=0)  # Everything is purgeable
        retention = RetentionManager(config, self.store, self.audit)
        # Insert old events
        for i in range(5):
            self.audit.record(AuditEventType.ORDER_SUBMITTED, "t", {"i": i})
        with tempfile.TemporaryDirectory() as tmpdir:
            result = retention.archive_and_purge(archive_dir=tmpdir, actor="admin")
            assert result["events_archived"] == 5
            assert result["events_purged"] == 5
            # Verify CSV was created
            csvs = [f for f in os.listdir(tmpdir) if f.endswith(".csv")]
            assert len(csvs) == 1

    def test_purge_without_archive(self):
        config = _default_config(retention_days=0)
        retention = RetentionManager(config, self.store, self.audit)
        self.audit.record(AuditEventType.ORDER_SUBMITTED, "t", {})
        result = retention.archive_and_purge(archive_dir=None)
        assert result["events_purged"] >= 1
        assert result["events_archived"] == 0

    def test_check_retention_status(self):
        config = _default_config(retention_days=365)
        retention = RetentionManager(config, self.store, self.audit)
        status = retention.check_retention_status()
        assert status["retention_days"] == 365
        assert "total_events" in status


# ===========================================================================
# ComplianceManager (orchestrator) tests
# ===========================================================================


class TestComplianceManager:
    def setup_method(self):
        self.db_path = _tmp_db()
        self.config = _default_config(
            db_path=self.db_path,
            approval_enabled=True,
            approval_threshold_notional=100.0,
        )
        self.mgr = ComplianceManager(self.config)

    def teardown_method(self):
        self.mgr.close()
        os.unlink(self.db_path)

    def test_check_order_approved(self):
        engine = FakeEngine()
        self.mgr.bind_engine(engine)
        result = self.mgr.check_order("m1", "buy", 0.5, 10, "trader")
        assert result == ComplianceAction.APPROVED

    def test_check_order_pending_approval(self):
        engine = FakeEngine()
        self.mgr.bind_engine(engine)
        result = self.mgr.check_order("m1", "buy", 0.5, 500, "trader")
        assert result == ComplianceAction.PENDING_APPROVAL

    def test_check_order_kill_switch(self):
        engine = FakeEngine()
        self.mgr.bind_engine(engine)
        self.mgr.kill_switch.activate("test", "risk")
        result = self.mgr.check_order("m1", "buy", 0.5, 10, "trader")
        assert result == ComplianceAction.KILL_SWITCH

    def test_check_order_restricted_market(self):
        config = _default_config(
            db_path=self.db_path,
            regulatory_limits=RegulatoryLimitConfig(restricted_markets={"m1"}),
        )
        mgr = ComplianceManager(config)
        engine = FakeEngine()
        mgr.bind_engine(engine)
        result = mgr.check_order("m1", "buy", 0.5, 10, "trader")
        assert result == ComplianceAction.REJECTED
        mgr.close()

    def test_record_fill(self):
        self.mgr.record_fill("m1", "buy", 10.0, 0.5)
        assert self.mgr.audit.count >= 1

    def test_record_cancel(self):
        self.mgr.record_cancel("ord1", "m1")

    def test_on_cycle(self):
        engine = FakeEngine()
        self.mgr.bind_engine(engine)
        alerts = self.mgr.on_cycle(engine)
        assert isinstance(alerts, list)

    def test_status(self):
        engine = FakeEngine()
        self.mgr.bind_engine(engine)
        status = self.mgr.status()
        assert "enabled" in status
        assert status["enabled"] is True
        assert "audit_events" in status
        assert "kill_switch" in status
        assert "pending_approvals" in status

    def test_verify_audit_integrity(self):
        self.mgr.record_fill("m1", "buy", 10.0, 0.5)
        valid, count = self.mgr.verify_audit_integrity()
        assert valid is True

    def test_export_audit(self):
        self.mgr.record_fill("m1", "buy", 10.0, 0.5)
        with tempfile.NamedTemporaryFile(suffix=".csv", delete=False) as f:
            csv_path = f.name
        try:
            count = self.mgr.export_audit(csv_path)
            assert count >= 1
        finally:
            os.unlink(csv_path)

    def test_daily_report(self):
        report = self.mgr.daily_report("2026-01-01")
        assert isinstance(report, DailyTradeReport)

    def test_default_config(self):
        """ComplianceManager works with default config."""
        mgr = ComplianceManager()
        assert mgr.config.enabled is True
        mgr.close()

    def test_disabled_check_order(self):
        """When compliance is disabled, all orders are approved."""
        config = ComplianceConfig(enabled=False)
        mgr = ComplianceManager(config, db_path=self.db_path)
        result = mgr.check_order("m1", "buy", 0.5, 9999)
        assert result == ComplianceAction.APPROVED
        mgr.close()


# ===========================================================================
# compliance_gate pipeline function tests
# ===========================================================================


class FakeContext:
    def __init__(self, params=None):
        self.params = params or {}


class TestComplianceGate:
    def test_noop_without_compliance(self):
        ctx = FakeContext()
        result = compliance_gate(ctx)
        assert result is ctx

    def test_returns_none_when_kill_switch_active(self):
        db_path = _tmp_db()
        config = _default_config(db_path=db_path)
        mgr = ComplianceManager(config)
        engine = FakeEngine()
        mgr.bind_engine(engine)
        mgr.kill_switch.activate("test", "risk")
        ctx = FakeContext(params={"compliance": mgr})
        result = compliance_gate(ctx)
        assert result is None
        mgr.close()
        os.unlink(db_path)

    def test_passes_through_when_active(self):
        db_path = _tmp_db()
        config = _default_config(db_path=db_path)
        mgr = ComplianceManager(config)
        engine = FakeEngine()
        mgr.bind_engine(engine)
        ctx = FakeContext(params={"compliance": mgr})
        result = compliance_gate(ctx)
        assert result is ctx
        mgr.close()
        os.unlink(db_path)

    def test_compliance_via_params(self):
        db_path = _tmp_db()
        config = _default_config(db_path=db_path)
        mgr = ComplianceManager(config)
        ctx = FakeContext(params={"compliance": mgr})
        result = compliance_gate(ctx)
        assert result is ctx
        mgr.close()
        os.unlink(db_path)

    def test_compliance_via_argument(self):
        db_path = _tmp_db()
        config = _default_config(db_path=db_path)
        mgr = ComplianceManager(config)
        ctx = FakeContext()
        result = compliance_gate(ctx, compliance=mgr)
        assert result is ctx
        mgr.close()
        os.unlink(db_path)


# ===========================================================================
# Types tests
# ===========================================================================


class TestTypes:
    def test_audit_event_to_dict(self):
        e = AuditEvent(
            event_id="e1", event_type=AuditEventType.ORDER_SUBMITTED,
            timestamp=1.0, actor="t", details={"k": "v"},
            event_hash="abc",
        )
        d = e.to_dict()
        assert d["event_id"] == "e1"
        assert d["event_type"] == "order_submitted"

    def test_surveillance_alert_to_dict(self):
        a = SurveillanceAlert(
            alert_id="a1", alert_type=SurveillanceAlertType.WASH_TRADING,
            severity="critical", timestamp=1.0, market_id="m1",
            description="wash", evidence={"key": "val"},
        )
        d = a.to_dict()
        assert d["alert_type"] == "wash_trading"

    def test_compliance_action_values(self):
        assert ComplianceAction.APPROVED.value == "approved"
        assert ComplianceAction.REJECTED.value == "rejected"
        assert ComplianceAction.PENDING_APPROVAL.value == "pending_approval"

    def test_role_values(self):
        assert Role.TRADER.value == "trader"
        assert Role.ADMIN.value == "admin"

    def test_regulatory_limit_config_defaults(self):
        config = RegulatoryLimitConfig()
        assert config.max_total_notional == float("inf")
        assert config.max_concentration_pct == 25.0
        assert config.max_orders_per_minute == 120

    def test_compliance_config_defaults(self):
        config = ComplianceConfig()
        assert config.enabled is True
        assert config.retention_days == 2555
        assert config.approval_enabled is False

    def test_audit_event_frozen(self):
        e = AuditEvent(
            event_id="e1", event_type=AuditEventType.ORDER_SUBMITTED,
            timestamp=1.0, actor="t", details={}, event_hash="h",
        )
        with pytest.raises(AttributeError):
            e.event_id = "e2"

    def test_surveillance_alert_frozen(self):
        a = SurveillanceAlert(
            alert_id="a1", alert_type=SurveillanceAlertType.SPOOFING,
            severity="warning", timestamp=1.0, market_id="m1",
            description="spoof", evidence={},
        )
        with pytest.raises(AttributeError):
            a.alert_id = "a2"


# ===========================================================================
# Integration tests
# ===========================================================================


class TestIntegration:
    """End-to-end compliance workflow tests."""

    def test_full_order_lifecycle(self):
        """Order -> approval -> fill -> surveillance -> report."""
        db_path = _tmp_db()
        config = _default_config(
            db_path=db_path,
            approval_enabled=True,
            approval_threshold_notional=50.0,
            regulatory_limits=RegulatoryLimitConfig(max_concentration_pct=100.0),
        )
        mgr = ComplianceManager(config)
        engine = FakeEngine([FakePosition("m1", "buy", 10.0, 0.5)])
        mgr.bind_engine(engine)

        # Step 1: Order below threshold -> auto-approved
        r = mgr.check_order("m1", "buy", 0.5, 10, "trader")
        assert r == ComplianceAction.APPROVED

        # Step 2: Order above threshold -> pending
        r = mgr.check_order("m1", "buy", 0.5, 200, "trader")
        assert r == ComplianceAction.PENDING_APPROVAL

        # Step 3: Officer approves
        pending = mgr.approval.pending()
        assert len(pending) == 1
        mgr.approval.approve(pending[0].request_id, "officer", "ok")

        # Step 4: Record fill
        mgr.record_fill("m1", "buy", 200, 0.5)

        # Step 5: Run surveillance cycle
        alerts = mgr.on_cycle(engine)

        # Step 6: Verify audit integrity
        valid, count = mgr.verify_audit_integrity()
        assert valid is True
        assert count > 0

        # Step 7: Generate report
        report = mgr.daily_report()
        assert isinstance(report, DailyTradeReport)

        mgr.close()
        os.unlink(db_path)

    def test_kill_switch_blocks_orders(self):
        db_path = _tmp_db()
        config = _default_config(db_path=db_path)
        mgr = ComplianceManager(config)
        engine = FakeEngine()
        mgr.bind_engine(engine)

        # Activate kill switch
        mgr.kill_switch.activate("market crash", "risk")

        # All orders should be blocked
        r = mgr.check_order("m1", "buy", 0.5, 10, "trader")
        assert r == ComplianceAction.KILL_SWITCH

        # Deactivate
        mgr.kill_switch.deactivate("risk", "market stabilized")
        r = mgr.check_order("m1", "buy", 0.5, 10, "trader")
        assert r == ComplianceAction.APPROVED

        mgr.close()
        os.unlink(db_path)

    def test_rbac_prevents_unauthorized_actions(self):
        db_path = _tmp_db()
        config = _default_config(db_path=db_path)
        mgr = ComplianceManager(config)
        engine = FakeEngine()
        mgr.bind_engine(engine)

        # Viewer cannot activate kill switch
        with pytest.raises(PermissionError):
            mgr.kill_switch.activate("test", "viewer")

        # Trader cannot assign roles
        with pytest.raises(PermissionError):
            mgr.access.assign_role("newbie", Role.TRADER, "trader")

        # Admin can assign roles
        mgr.access.assign_role("newbie", Role.TRADER, "admin")
        assert mgr.access.get_role("newbie") == Role.TRADER

        mgr.close()
        os.unlink(db_path)

    def test_retention_lifecycle(self):
        db_path = _tmp_db()
        config = _default_config(db_path=db_path, retention_days=0)
        mgr = ComplianceManager(config)

        # Record some events
        for i in range(3):
            mgr.record_fill(f"m{i}", "buy", 10.0, 0.5)

        # Archive and purge
        with tempfile.TemporaryDirectory() as tmpdir:
            result = mgr.retention.archive_and_purge(archive_dir=tmpdir, actor="admin")
            assert result["events_archived"] >= 3

        mgr.close()
        os.unlink(db_path)
