"""Tests for channel operations with integrated audit logging.

Tests sync, share, retract, and review operations including their
audit trail generation, permission checks, and security event mirroring.
"""
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 CHANNEL_AUDIT_SCHEMA, ChannelAuditLogger
from greymatter_core.channel_ops import ChannelOperations, OperationResult


@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 db_with_channels(db):
    """Database with test channels and some data."""
    db.execute(
        "INSERT INTO solo_channels (id, name, classification, encrypted, sync_enabled) "
        "VALUES (?, ?, ?, ?, ?)",
        ("ch-team", "Team Channel", "team", 1, 1),
    )
    db.execute(
        "INSERT INTO solo_channels (id, name, classification, encrypted, sync_enabled) "
        "VALUES (?, ?, ?, ?, ?)",
        ("ch-personal", "Personal", "personal", 1, 0),
    )
    db.execute(
        "INSERT INTO solo_channels (id, name, classification, encrypted, sync_enabled) "
        "VALUES (?, ?, ?, ?, ?)",
        ("ch-public", "Public Data", "public", 0, 1),
    )
    return db


@pytest.fixture
def ops(db_with_channels):
    """Channel operations bound to the test database."""
    return ChannelOperations(
        db_with_channels,
        actor="test-user",
        actor_type="user",
    )


@pytest.fixture
def agent_ops(db_with_channels):
    """Channel operations from an AI agent."""
    return ChannelOperations(
        db_with_channels,
        actor="dispatch-agent-1",
        actor_type="agent",
    )


# =========================================================================
# OperationResult
# =========================================================================


class TestOperationResult:

    def test_to_dict_success(self):
        result = OperationResult(
            success=True,
            operation="sync",
            channel_id="ch-test",
            items_affected=10,
            detail="Synced 10 items",
            audit_event_id="cae-123",
        )
        d = result.to_dict()
        assert d["success"] is True
        assert d["audit_event_id"] == "cae-123"
        assert "error" not in d

    def test_to_dict_failure(self):
        result = OperationResult(
            success=False,
            operation="share",
            channel_id="ch-personal",
            error="personal_channel_share_denied",
        )
        d = result.to_dict()
        assert d["success"] is False
        assert d["error"] == "personal_channel_share_denied"


# =========================================================================
# Sync
# =========================================================================


class TestSyncChannel:

    def test_sync_success(self, ops):
        result = ops.sync_channel("ch-team", direction="push", target_node="node-2")
        assert result.success is True
        assert result.operation == "sync"
        assert result.audit_event_id is not None
        assert result.audit_event_id.startswith("cae-")

    def test_sync_bidirectional(self, ops):
        result = ops.sync_channel("ch-team", direction="bidirectional")
        assert result.success is True
        assert "bidirectional" in result.detail

    def test_sync_nonexistent_channel(self, ops):
        result = ops.sync_channel("ch-nonexistent", direction="push")
        assert result.success is False
        assert result.error == "channel_not_found"

    def test_sync_disabled_channel(self, ops):
        result = ops.sync_channel("ch-personal", direction="push")
        assert result.success is False
        assert result.error == "sync_disabled"

    def test_sync_with_data_scope(self, ops):
        result = ops.sync_channel(
            "ch-team",
            direction="push",
            data_scope="knowledge,patterns",
        )
        assert result.success is True

    def test_sync_creates_audit_event(self, ops, db_with_channels):
        result = ops.sync_channel("ch-team", direction="push")
        audit = ChannelAuditLogger(db_with_channels)
        events = audit.get_events(channel_id="ch-team", operation="sync")
        assert len(events) >= 1
        assert events[0].direction == "push"
        assert events[0].actor == "test-user"
        assert events[0].actor_type == "user"


# =========================================================================
# Share
# =========================================================================


class TestShareChannel:

    def test_share_success(self, ops):
        result = ops.share_channel("ch-team", target_node="node-2")
        assert result.success is True
        assert result.operation == "share"

    def test_share_with_data_scope(self, ops):
        result = ops.share_channel(
            "ch-team",
            target_node="node-2",
            data_scope="knowledge",
        )
        assert result.success is True

    def test_share_personal_denied(self, ops):
        result = ops.share_channel("ch-personal", target_node="node-2")
        assert result.success is False
        assert result.error == "personal_channel_share_denied"

    def test_share_personal_creates_security_event(self, ops, db_with_channels):
        ops.share_channel("ch-personal", target_node="node-2")
        rows = db_with_channels.execute(
            "SELECT * FROM security_events WHERE event_type = 'channel_share'"
        )
        assert len(rows) == 1
        assert rows[0]["action_taken"] == "denied"
        assert rows[0]["severity"] == "high"

    def test_share_nonexistent_channel(self, ops):
        result = ops.share_channel("ch-missing", target_node="node-2")
        assert result.success is False
        assert result.error == "channel_not_found"

    def test_share_creates_audit_event(self, ops, db_with_channels):
        ops.share_channel("ch-team", target_node="node-2")
        audit = ChannelAuditLogger(db_with_channels)
        events = audit.get_events(operation="share")
        assert len(events) >= 1
        assert events[0].target_node == "node-2"
        assert events[0].direction == "outbound"


# =========================================================================
# Retract
# =========================================================================


class TestRetractChannel:

    def test_retract_success(self, ops):
        result = ops.retract_channel("ch-team", target_node="node-2")
        assert result.success is True
        assert result.operation == "retract"

    def test_retract_with_reason(self, ops):
        result = ops.retract_channel(
            "ch-team",
            target_node="node-2",
            reason="Data correction required",
        )
        assert result.success is True
        assert "Data correction required" in result.detail

    def test_retract_all_nodes(self, ops):
        result = ops.retract_channel("ch-team")
        assert result.success is True
        assert "all nodes" in result.detail

    def test_retract_nonexistent_channel(self, ops):
        result = ops.retract_channel("ch-missing")
        assert result.success is False
        assert result.error == "channel_not_found"

    def test_retract_creates_security_event(self, ops, db_with_channels):
        ops.retract_channel("ch-team", target_node="node-2")
        rows = db_with_channels.execute(
            "SELECT * FROM security_events WHERE event_type = 'channel_retract'"
        )
        assert len(rows) == 1
        assert rows[0]["severity"] == "high"

    def test_retract_audit_severity_is_high(self, ops, db_with_channels):
        ops.retract_channel("ch-team")
        audit = ChannelAuditLogger(db_with_channels)
        events = audit.get_events(operation="retract")
        assert len(events) >= 1
        assert events[0].severity == "high"


# =========================================================================
# Review
# =========================================================================


class TestReviewChannel:

    def test_review_approved(self, ops):
        result = ops.review_channel("ch-team", verdict="approved")
        assert result.success is True
        assert "approved" in result.detail

    def test_review_rejected(self, ops):
        result = ops.review_channel(
            "ch-team",
            verdict="rejected",
            notes="Stale data found",
        )
        assert result.success is False
        assert "Stale data found" in result.detail

    def test_review_pending(self, ops):
        result = ops.review_channel("ch-team", verdict="pending")
        assert result.success is True  # pending is not a failure

    def test_review_nonexistent_channel(self, ops):
        result = ops.review_channel("ch-missing")
        assert result.success is False
        assert result.error == "channel_not_found"

    def test_review_with_items_and_scope(self, ops):
        result = ops.review_channel(
            "ch-team",
            items_reviewed=15,
            data_scope="knowledge",
        )
        assert result.success is True
        assert result.items_affected == 15

    def test_review_creates_audit_event(self, ops, db_with_channels):
        ops.review_channel(
            "ch-team",
            verdict="approved",
            notes="All clear",
        )
        audit = ChannelAuditLogger(db_with_channels)
        events = audit.get_events(operation="review")
        assert len(events) >= 1
        assert "approved" in events[0].detail


# =========================================================================
# Agent operations — AI actor tracking
# =========================================================================


class TestAgentOperations:

    def test_agent_sync_tracked(self, agent_ops, db_with_channels):
        agent_ops.sync_channel("ch-team", direction="push")
        audit = ChannelAuditLogger(db_with_channels)
        events = audit.get_events(operation="sync")
        assert events[0].actor == "dispatch-agent-1"
        assert events[0].actor_type == "agent"

    def test_agent_review_in_replay(self, agent_ops, db_with_channels):
        agent_ops.review_channel("ch-team", verdict="approved")
        audit = ChannelAuditLogger(db_with_channels)
        replay_events = audit.replay(ai_only=True)
        assert len(replay_events) == 1
        assert replay_events[0].actor_type == "agent"

    def test_user_events_excluded_from_ai_replay(self, ops, db_with_channels):
        ops.sync_channel("ch-team", direction="push")
        audit = ChannelAuditLogger(db_with_channels)
        replay_events = audit.replay(ai_only=True)
        assert len(replay_events) == 0


# =========================================================================
# Migration test
# =========================================================================


class TestMigration:

    def test_migration_creates_table(self):
        """Migration 007 creates the channel_audit_log table."""
        d = SQLiteDatabase(":memory:")
        # Migration should have already run via db.migrate()
        # But we need solo schema for channels
        d._conn.executescript(get_solo_ddl())
        d._conn.commit()

        # Import and run migration manually
        from greymatter_core.schema.migrations import _MIGRATIONS, _discover_migrations
        _discover_migrations()
        assert 7 in _MIGRATIONS
        desc, fn = _MIGRATIONS[7]
        assert "channel_audit_log" in desc
        fn(d)

        # Verify table exists
        tables = d.execute(
            "SELECT name FROM sqlite_master WHERE type='table' AND name='channel_audit_log'"
        )
        assert len(tables) == 1

        # Verify indexes exist
        indexes = d.execute(
            "SELECT name FROM sqlite_master WHERE type='index' AND name LIKE 'idx_channel_audit%'"
        )
        assert len(indexes) >= 4
