"""Tests for channel audit logging system.

Tests the full audit lifecycle: logging, querying, replay, formatting,
event chains, security_events mirroring, and statistics.
"""
import json

import pytest

from greymatter_core.db import SQLiteDatabase
from greymatter_core.schema.solo import get_sqlite_ddl as get_solo_ddl
from greymatter_core.channel_audit import (
    AuditEvent,
    AuditOutcome,
    AuditSeverity,
    CHANNEL_AUDIT_SCHEMA,
    ChannelAuditLogger,
    ChannelOperation,
    format_event,
    format_events,
    format_replay_report,
)


@pytest.fixture
def db():
    """In-memory SQLite database with core + solo + channel_audit schema."""
    d = SQLiteDatabase(":memory:")
    d._conn.executescript(get_solo_ddl())
    d._conn.executescript(CHANNEL_AUDIT_SCHEMA)
    d._conn.commit()
    return d


@pytest.fixture
def audit(db):
    """Channel audit logger bound to the test database."""
    return ChannelAuditLogger(db)


@pytest.fixture
def db_with_channel(db):
    """Database with a test channel created."""
    db.execute(
        "INSERT INTO solo_channels (id, name, classification, encrypted, sync_enabled) "
        "VALUES (?, ?, ?, ?, ?)",
        ("ch-test", "Test Channel", "team", 1, 1),
    )
    db.execute(
        "INSERT INTO solo_channels (id, name, classification, encrypted, sync_enabled) "
        "VALUES (?, ?, ?, ?, ?)",
        ("ch-personal", "Personal", "personal", 1, 0),
    )
    return db


# =========================================================================
# AuditEvent dataclass
# =========================================================================


class TestAuditEvent:

    def test_to_dict_basic(self):
        event = AuditEvent(
            id="cae-12345678",
            channel_id="ch-test",
            operation="sync",
        )
        d = event.to_dict()
        assert d["id"] == "cae-12345678"
        assert d["channel_id"] == "ch-test"
        assert d["operation"] == "sync"
        assert d["severity"] == "info"
        assert d["outcome"] == "success"

    def test_to_dict_with_payload(self):
        event = AuditEvent(
            id="cae-12345678",
            channel_id="ch-test",
            operation="share",
            payload={"items": ["k-1", "k-2"], "encrypted": True},
        )
        d = event.to_dict()
        assert json.loads(d["payload"]) == {"items": ["k-1", "k-2"], "encrypted": True}

    def test_to_dict_null_payload(self):
        event = AuditEvent(id="cae-1", channel_id="ch-1", operation="sync")
        assert event.to_dict()["payload"] is None

    def test_from_row_basic(self):
        row = {
            "id": "cae-test",
            "channel_id": "ch-1",
            "operation": "review",
            "severity": "warn",
            "outcome": "pending",
            "actor": "agent-1",
            "actor_type": "agent",
            "target_node": None,
            "direction": None,
            "data_scope": "knowledge",
            "items_affected": 5,
            "detail": "Pending review",
            "payload": '{"verdict": "pending"}',
            "session_id": "s-1",
            "node_id": "n-1",
            "parent_event_id": None,
            "created_at": "2026-03-22 14:00:00",
        }
        event = AuditEvent.from_row(row)
        assert event.id == "cae-test"
        assert event.operation == "review"
        assert event.payload == {"verdict": "pending"}
        assert event.items_affected == 5

    def test_from_row_invalid_json_payload(self):
        row = {
            "id": "cae-x",
            "channel_id": "ch-1",
            "operation": "sync",
            "payload": "not-json",
        }
        event = AuditEvent.from_row(row)
        assert event.payload == {"raw": "not-json"}

    def test_from_row_none_payload(self):
        row = {
            "id": "cae-x",
            "channel_id": "ch-1",
            "operation": "sync",
            "payload": None,
        }
        event = AuditEvent.from_row(row)
        assert event.payload is None


# =========================================================================
# Enums
# =========================================================================


class TestEnums:

    def test_channel_operations(self):
        assert ChannelOperation.SYNC.value == "sync"
        assert ChannelOperation.SHARE.value == "share"
        assert ChannelOperation.RETRACT.value == "retract"
        assert ChannelOperation.REVIEW.value == "review"

    def test_audit_severity(self):
        assert AuditSeverity.INFO.value == "info"
        assert AuditSeverity.CRITICAL.value == "critical"

    def test_audit_outcome(self):
        assert AuditOutcome.SUCCESS.value == "success"
        assert AuditOutcome.DENIED.value == "denied"


# =========================================================================
# ChannelAuditLogger — writing events
# =========================================================================


class TestAuditLoggerWrite:

    def test_log_sync_success(self, audit):
        event_id = audit.log_sync(
            "ch-test",
            "push",
            items_affected=42,
            target_node="node-2",
            data_scope="knowledge,patterns",
            detail="Full sync completed",
            actor="dispatch-1",
            actor_type="daemon",
        )
        assert event_id is not None
        assert event_id.startswith("cae-")

    def test_log_sync_failure(self, audit):
        event_id = audit.log_sync(
            "ch-test",
            "pull",
            outcome="failed",
            detail="Connection refused",
        )
        assert event_id is not None

    def test_log_share(self, audit):
        event_id = audit.log_share(
            "ch-team",
            "node-3",
            items_affected=10,
            data_scope="knowledge",
            actor="user-keith",
            actor_type="user",
        )
        assert event_id is not None

    def test_log_retract(self, audit):
        event_id = audit.log_retract(
            "ch-team",
            items_affected=5,
            target_node="node-3",
            detail="Retracting stale data",
            payload={"reason": "data_correction"},
        )
        assert event_id is not None

    def test_log_review(self, audit):
        event_id = audit.log_review(
            "ch-team",
            outcome="success",
            detail="All items verified",
            actor="reviewer-1",
            actor_type="agent",
            items_affected=20,
        )
        assert event_id is not None

    def test_log_with_parent_event(self, audit):
        parent_id = audit.log_sync(
            "ch-test", "push", detail="Parent sync"
        )
        child_id = audit.log_sync(
            "ch-test",
            "push",
            detail="Sub-sync: knowledge table",
            parent_event_id=parent_id,
        )
        assert child_id is not None

    def test_log_with_payload(self, audit):
        event_id = audit.log_sync(
            "ch-test",
            "push",
            payload={"version_before": 5, "version_after": 8, "conflicts": 0},
        )
        assert event_id is not None


# =========================================================================
# ChannelAuditLogger — querying events
# =========================================================================


class TestAuditLoggerQuery:

    def _seed_events(self, audit):
        """Seed a mix of events for query tests."""
        audit.log_sync("ch-a", "push", items_affected=10, actor="agent-1", actor_type="agent")
        audit.log_sync("ch-a", "pull", outcome="failed", detail="timeout")
        audit.log_share("ch-b", "node-2", items_affected=5)
        audit.log_retract("ch-a", items_affected=3, target_node="node-2")
        audit.log_review("ch-b", outcome="success", actor="user-1", actor_type="user")
        audit.log_review("ch-a", outcome="failed", actor="agent-2", actor_type="agent",
                         detail="Inconsistencies found")

    def test_get_events_all(self, audit):
        self._seed_events(audit)
        events = audit.get_events()
        assert len(events) == 6

    def test_get_events_by_channel(self, audit):
        self._seed_events(audit)
        events = audit.get_events(channel_id="ch-a")
        assert len(events) == 4
        assert all(e.channel_id == "ch-a" for e in events)

    def test_get_events_by_operation(self, audit):
        self._seed_events(audit)
        events = audit.get_events(operation="sync")
        assert len(events) == 2

    def test_get_events_by_outcome(self, audit):
        self._seed_events(audit)
        events = audit.get_events(outcome="failed")
        assert len(events) == 2

    def test_get_events_by_actor(self, audit):
        self._seed_events(audit)
        events = audit.get_events(actor="agent-1")
        assert len(events) == 1
        assert events[0].actor == "agent-1"

    def test_get_events_with_limit(self, audit):
        self._seed_events(audit)
        events = audit.get_events(limit=2)
        assert len(events) == 2

    def test_get_events_newest_first(self, audit):
        self._seed_events(audit)
        events = audit.get_events()
        # created_at should be in descending order
        timestamps = [e.created_at for e in events if e.created_at]
        assert timestamps == sorted(timestamps, reverse=True)


# =========================================================================
# ChannelAuditLogger — replay
# =========================================================================


class TestAuditLoggerReplay:

    def _seed_events(self, audit):
        audit.log_sync("ch-a", "push", actor="agent-1", actor_type="agent")
        audit.log_share("ch-a", "node-2", actor="daemon-1", actor_type="daemon")
        audit.log_review("ch-a", actor="user-1", actor_type="user")
        audit.log_retract("ch-a", actor="agent-2", actor_type="ai")

    def test_replay_returns_chronological_order(self, audit):
        self._seed_events(audit)
        events = audit.replay(channel_id="ch-a")
        timestamps = [e.created_at for e in events if e.created_at]
        assert timestamps == sorted(timestamps)

    def test_replay_ai_only(self, audit):
        self._seed_events(audit)
        events = audit.replay(ai_only=True)
        assert len(events) == 3  # agent, daemon, ai — not user
        for e in events:
            assert e.actor_type in ("agent", "daemon", "ai")

    def test_replay_with_limit(self, audit):
        self._seed_events(audit)
        events = audit.replay(limit=2)
        assert len(events) == 2

    def test_replay_empty(self, audit):
        events = audit.replay(channel_id="ch-nonexistent")
        assert events == []


# =========================================================================
# ChannelAuditLogger — event chains
# =========================================================================


class TestEventChains:

    def test_get_event_chain(self, audit):
        parent_id = audit.log_sync("ch-test", "push", detail="Parent")
        child1 = audit.log_sync(
            "ch-test", "push", detail="Child 1: knowledge",
            parent_event_id=parent_id,
        )
        child2 = audit.log_sync(
            "ch-test", "push", detail="Child 2: patterns",
            parent_event_id=parent_id,
        )
        chain = audit.get_event_chain(parent_id)
        assert len(chain) == 3
        assert chain[0].id == parent_id
        assert chain[0].detail == "Parent"

    def test_get_event_chain_no_children(self, audit):
        event_id = audit.log_sync("ch-test", "push")
        chain = audit.get_event_chain(event_id)
        assert len(chain) == 1

    def test_get_event_chain_nonexistent(self, audit):
        chain = audit.get_event_chain("cae-nonexistent")
        assert chain == []


# =========================================================================
# ChannelAuditLogger — statistics
# =========================================================================


class TestAuditStats:

    def _seed_events(self, audit):
        audit.log_sync("ch-a", "push", items_affected=10)
        audit.log_sync("ch-a", "pull", outcome="failed")
        audit.log_share("ch-a", "node-2", items_affected=5)
        audit.log_retract("ch-a", items_affected=3)
        audit.log_review("ch-a", actor="agent-1", actor_type="agent")

    def test_get_stats_total(self, audit):
        self._seed_events(audit)
        stats = audit.get_stats()
        assert stats["total_events"] == 5

    def test_get_stats_by_operation(self, audit):
        self._seed_events(audit)
        stats = audit.get_stats()
        assert stats["by_operation"]["sync"] == 2
        assert stats["by_operation"]["share"] == 1
        assert stats["by_operation"]["retract"] == 1
        assert stats["by_operation"]["review"] == 1

    def test_get_stats_by_outcome(self, audit):
        self._seed_events(audit)
        stats = audit.get_stats()
        assert stats["by_outcome"]["success"] >= 3
        assert stats["by_outcome"]["failed"] == 1

    def test_get_stats_ai_count(self, audit):
        self._seed_events(audit)
        stats = audit.get_stats()
        assert stats["ai_decision_count"] == 1

    def test_get_stats_scoped_to_channel(self, audit):
        audit.log_sync("ch-a", "push")
        audit.log_sync("ch-b", "push")
        stats = audit.get_stats(channel_id="ch-a")
        assert stats["total_events"] == 1

    def test_get_stats_recent_failures(self, audit):
        self._seed_events(audit)
        stats = audit.get_stats()
        assert len(stats["recent_failures"]) == 1
        assert stats["recent_failures"][0]["outcome"] == "failed"

    def test_get_stats_empty(self, audit):
        stats = audit.get_stats()
        assert stats["total_events"] == 0


# =========================================================================
# Security events mirroring
# =========================================================================


class TestSecurityEventsMirroring:

    def test_mirror_to_security_events(self, audit, db):
        event = AuditEvent(
            id="cae-mirror-1",
            channel_id="ch-test",
            operation="retract",
            severity="high",
            outcome="success",
            detail="Retracted 5 items from node-2",
        )
        audit.log_to_security_events(event)

        rows = db.execute(
            "SELECT * FROM security_events WHERE event_type = 'channel_retract'"
        )
        assert len(rows) == 1
        assert rows[0]["severity"] == "high"
        assert rows[0]["tool_name"] == "channel:ch-test"
        assert rows[0]["action_taken"] == "success"

    def test_mirror_denied_share(self, audit, db):
        event = AuditEvent(
            id="cae-mirror-2",
            channel_id="ch-personal",
            operation="share",
            severity="high",
            outcome="denied",
            detail="Blocked: share of personal channel",
        )
        audit.log_to_security_events(event)

        rows = db.execute(
            "SELECT * FROM security_events WHERE event_type = 'channel_share'"
        )
        assert len(rows) == 1
        assert rows[0]["action_taken"] == "denied"


# =========================================================================
# Fail-silent behavior
# =========================================================================


class TestFailSilent:

    def test_fail_silent_on_missing_table(self):
        """Logger doesn't crash if channel_audit_log table is missing."""
        d = SQLiteDatabase(":memory:")
        # Don't create channel_audit_log table
        logger = ChannelAuditLogger(d, fail_silent=True)
        result = logger.log_sync("ch-test", "push")
        assert result is None  # Failed silently

    def test_fail_loud_on_missing_table(self):
        """Logger raises when fail_silent=False."""
        d = SQLiteDatabase(":memory:")
        logger = ChannelAuditLogger(d, fail_silent=False)
        with pytest.raises(Exception):
            logger.log_sync("ch-test", "push")


# =========================================================================
# Append-only integrity
# =========================================================================


class TestAppendOnly:

    def test_ids_are_unique(self, audit):
        ids = set()
        for i in range(20):
            event_id = audit.log_sync("ch-test", "push", detail=f"event-{i}")
            assert event_id not in ids
            ids.add(event_id)

    def test_events_are_ordered_by_creation(self, audit):
        for i in range(10):
            audit.log_sync("ch-test", "push", detail=f"event-{i}")
        events = audit.get_events(channel_id="ch-test")
        # Newest first from get_events
        for i, event in enumerate(events[:-1]):
            assert event.created_at >= events[i + 1].created_at


# =========================================================================
# Human-readable formatting
# =========================================================================


class TestFormatting:

    def test_format_event_basic(self):
        event = AuditEvent(
            id="cae-1",
            channel_id="ch-personal",
            operation="sync",
            severity="info",
            outcome="success",
            direction="push",
            items_affected=12,
            created_at="2026-03-22 14:32:01",
        )
        line = format_event(event)
        assert "[2026-03-22 14:32:01]" in line
        assert "OK" in line
        assert "ch-personal" in line
        assert "synced" in line
        assert "push" in line
        assert "12 items" in line

    def test_format_event_failure(self):
        event = AuditEvent(
            id="cae-2",
            channel_id="ch-team",
            operation="share",
            severity="high",
            outcome="denied",
            target_node="node-2",
            detail="Insufficient permissions",
            created_at="2026-03-22 14:32:05",
        )
        line = format_event(event)
        assert "DENIED" in line
        assert "!!" in line
        assert "-> node-2" in line
        assert "Insufficient permissions" in line

    def test_format_event_with_actor(self):
        event = AuditEvent(
            id="cae-3",
            channel_id="ch-test",
            operation="review",
            actor="agent-dispatch-1",
            actor_type="agent",
            created_at="2026-03-22 14:33:00",
        )
        line = format_event(event)
        assert "agent:agent-dispatch-1" in line

    def test_format_events_returns_list(self):
        events = [
            AuditEvent(id="cae-1", channel_id="ch-1", operation="sync",
                       created_at="2026-03-22 14:00:00"),
            AuditEvent(id="cae-2", channel_id="ch-2", operation="share",
                       created_at="2026-03-22 14:01:00"),
        ]
        lines = format_events(events)
        assert len(lines) == 2
        assert all(isinstance(l, str) for l in lines)

    def test_format_replay_report_empty(self):
        report = format_replay_report([])
        assert "No audit events" in report

    def test_format_replay_report_with_events(self):
        events = [
            AuditEvent(
                id="cae-1", channel_id="ch-test", operation="sync",
                outcome="success", actor="agent-1", actor_type="agent",
                payload={"version": 3},
                created_at="2026-03-22 14:00:00",
            ),
            AuditEvent(
                id="cae-2", channel_id="ch-test", operation="retract",
                outcome="failed", severity="critical",
                created_at="2026-03-22 14:01:00",
            ),
        ]
        report = format_replay_report(events)
        assert "CHANNEL AUDIT REPLAY REPORT" in report
        assert "Events: 2" in report
        assert "SUMMARY" in report
        assert "AI/AGENT DECISIONS" in report
        assert "FAILURES / DENIALS" in report
        assert "FULL TIMELINE" in report

    def test_format_event_truncates_long_timestamp(self):
        event = AuditEvent(
            id="cae-1", channel_id="ch-1", operation="sync",
            created_at="2026-03-22T14:32:01.123456+00:00",
        )
        line = format_event(event)
        assert "2026-03-22T14:32:01" in line
        assert "123456" not in line
