"""Tests for the audit logging module."""

from __future__ import annotations

import json
import sqlite3
import tempfile
from datetime import datetime, timedelta
from pathlib import Path
from unittest.mock import MagicMock

import pytest

from oclawma.audit import (
    AuditAction,
    AuditEvent,
    AuditLogger,
    RetentionPolicy,
    TamperDetectionError,
    configure_global_audit_logger,
    get_global_audit_logger,
    log_audit,
)

# =============================================================================
# Fixtures
# =============================================================================


@pytest.fixture
def temp_audit_path() -> Path:
    """Create a temporary audit database file path."""
    with tempfile.NamedTemporaryFile(suffix="_audit.db", delete=False) as f:
        path = Path(f.name)
    yield path
    if path.exists():
        path.unlink()


@pytest.fixture
def audit_logger(temp_audit_path: Path) -> AuditLogger:
    """Provide an audit logger with temporary database."""
    logger = AuditLogger(temp_audit_path)
    yield logger
    logger.close()


@pytest.fixture
def mock_event_handler():
    """Provide a mock event handler."""
    return MagicMock()


# =============================================================================
# AuditAction Enum Tests
# =============================================================================


class TestAuditAction:
    """Tests for AuditAction enum."""

    def test_action_values(self):
        """Test that actions have correct values."""
        assert AuditAction.CREATE.value == "create"
        assert AuditAction.UPDATE.value == "update"
        assert AuditAction.DELETE.value == "delete"
        assert AuditAction.CANCEL.value == "cancel"
        assert AuditAction.COMPLETE.value == "complete"
        assert AuditAction.FAIL.value == "fail"

    def test_action_from_string(self):
        """Test creating action from string."""
        assert AuditAction("create") == AuditAction.CREATE
        assert AuditAction("update") == AuditAction.UPDATE


# =============================================================================
# AuditEvent Tests
# =============================================================================


class TestAuditEvent:
    """Tests for AuditEvent dataclass."""

    def test_default_event(self):
        """Test creating event with defaults."""
        event = AuditEvent()
        assert event.id is None
        assert event.action == AuditAction.CREATE
        assert event.user == "system"
        assert event.job_id is None
        assert event.details == {}
        assert event.hash == ""
        assert event.previous_hash == ""

    def test_event_to_dict(self):
        """Test converting event to dictionary."""
        event = AuditEvent(
            id=1,
            timestamp=datetime(2024, 1, 1, 12, 0, 0),
            action=AuditAction.CREATE,
            user="admin",
            job_id=123,
            details={"key": "value"},
            hash="abc123",
            previous_hash="def456",
        )
        data = event.to_dict()
        assert data["id"] == 1
        assert data["action"] == "create"
        assert data["user"] == "admin"
        assert data["job_id"] == 123
        assert data["details"] == {"key": "value"}
        assert data["hash"] == "abc123"
        assert data["previous_hash"] == "def456"


# =============================================================================
# AuditLogger Basic Tests
# =============================================================================


class TestAuditLoggerBasics:
    """Tests for basic AuditLogger functionality."""

    def test_init_creates_database(self, temp_audit_path: Path):
        """Test that initialization creates the database file."""
        logger = AuditLogger(temp_audit_path)
        assert temp_audit_path.exists()
        logger.close()

    def test_init_creates_schema(self, temp_audit_path: Path):
        """Test that initialization creates the schema."""
        logger = AuditLogger(temp_audit_path)
        logger.close()

        # Verify tables exist
        conn = sqlite3.connect(str(temp_audit_path))
        cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table'")
        tables = {row[0] for row in cursor.fetchall()}
        assert "audit_events" in tables
        assert "audit_config" in tables
        conn.close()

    def test_init_creates_indexes(self, temp_audit_path: Path):
        """Test that initialization creates indexes."""
        logger = AuditLogger(temp_audit_path)
        logger.close()

        # Verify indexes exist
        conn = sqlite3.connect(str(temp_audit_path))
        cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='index'")
        indexes = {row[0] for row in cursor.fetchall()}
        assert "idx_audit_timestamp" in indexes
        assert "idx_audit_action" in indexes
        assert "idx_audit_user" in indexes
        assert "idx_audit_job_id" in indexes
        conn.close()


# =============================================================================
# AuditLogger Log Tests
# =============================================================================


class TestAuditLoggerLog:
    """Tests for logging functionality."""

    def test_log_basic_event(self, audit_logger: AuditLogger):
        """Test logging a basic event."""
        event = audit_logger.log(AuditAction.CREATE, user="admin", job_id=123)

        assert event.id is not None
        assert event.action == AuditAction.CREATE
        assert event.user == "admin"
        assert event.job_id == 123
        assert event.hash is not None
        assert len(event.hash) == 64  # SHA-256 hex

    def test_log_with_details(self, audit_logger: AuditLogger):
        """Test logging with details."""
        details = {"task": "backup", "target": "/data"}
        event = audit_logger.log(AuditAction.CREATE, user="admin", details=details)

        assert event.details == details

    def test_log_assigns_increasing_ids(self, audit_logger: AuditLogger):
        """Test that logs get increasing IDs."""
        event1 = audit_logger.log(AuditAction.CREATE)
        event2 = audit_logger.log(AuditAction.UPDATE)
        event3 = audit_logger.log(AuditAction.DELETE)

        assert event1.id < event2.id < event3.id

    def test_log_calls_event_handler(self, temp_audit_path: Path):
        """Test that event handler is called."""
        handler = MagicMock()
        logger = AuditLogger(temp_audit_path, on_event=handler)

        event = logger.log(AuditAction.CREATE)

        handler.assert_called_once()
        assert handler.call_args[0][0].id == event.id
        logger.close()

    def test_log_handler_exception_ignored(self, temp_audit_path: Path):
        """Test that handler exceptions are ignored."""
        handler = MagicMock(side_effect=Exception("Handler error"))
        logger = AuditLogger(temp_audit_path, on_event=handler)

        # Should not raise
        event = logger.log(AuditAction.CREATE)
        assert event is not None
        logger.close()

    def test_log_all_actions(self, audit_logger: AuditLogger):
        """Test logging all action types."""
        for action in AuditAction:
            event = audit_logger.log(action)
            assert event.action == action


# =============================================================================
# AuditLogger Hash Chain Tests
# =============================================================================


class TestAuditLoggerHashChain:
    """Tests for hash chain integrity."""

    def test_first_event_genesis_hash(self, audit_logger: AuditLogger):
        """Test that first event uses genesis previous_hash."""
        event = audit_logger.log(AuditAction.CREATE)
        assert event.previous_hash == "0" * 64

    def test_subsequent_events_link_hashes(self, audit_logger: AuditLogger):
        """Test that events form a hash chain."""
        event1 = audit_logger.log(AuditAction.CREATE)
        event2 = audit_logger.log(AuditAction.UPDATE)

        assert event2.previous_hash == event1.hash

    def test_hash_includes_all_fields(self, audit_logger: AuditLogger):
        """Test that hash includes all event fields."""
        event = audit_logger.log(
            AuditAction.CREATE,
            user="admin",
            job_id=123,
            details={"key": "value"},
        )

        # Hash should be non-empty and consistent
        assert len(event.hash) == 64

        # Verify integrity passes
        is_valid, _ = audit_logger.verify_integrity()
        assert is_valid is True


# =============================================================================
# AuditLogger Query Tests
# =============================================================================


class TestAuditLoggerQuery:
    """Tests for query functionality."""

    def test_query_returns_all_events(self, audit_logger: AuditLogger):
        """Test querying returns all events."""
        audit_logger.log(AuditAction.CREATE)
        audit_logger.log(AuditAction.UPDATE)
        audit_logger.log(AuditAction.DELETE)

        events = audit_logger.query()
        assert len(events) == 3

    def test_query_by_action(self, audit_logger: AuditLogger):
        """Test filtering by action."""
        audit_logger.log(AuditAction.CREATE)
        audit_logger.log(AuditAction.CREATE)
        audit_logger.log(AuditAction.UPDATE)

        events = audit_logger.query(action=AuditAction.CREATE)
        assert len(events) == 2
        for event in events:
            assert event.action == AuditAction.CREATE

    def test_query_by_user(self, audit_logger: AuditLogger):
        """Test filtering by user."""
        audit_logger.log(AuditAction.CREATE, user="admin")
        audit_logger.log(AuditAction.CREATE, user="user1")
        audit_logger.log(AuditAction.UPDATE, user="admin")

        events = audit_logger.query(user="admin")
        assert len(events) == 2
        for event in events:
            assert event.user == "admin"

    def test_query_by_job_id(self, audit_logger: AuditLogger):
        """Test filtering by job_id."""
        audit_logger.log(AuditAction.CREATE, job_id=1)
        audit_logger.log(AuditAction.UPDATE, job_id=2)
        audit_logger.log(AuditAction.DELETE, job_id=1)

        events = audit_logger.query(job_id=1)
        assert len(events) == 2
        for event in events:
            assert event.job_id == 1

    def test_query_by_date_range(self, audit_logger: AuditLogger):
        """Test filtering by date range."""
        now = datetime.utcnow()

        # Log events at different times (simulated)
        audit_logger.log(AuditAction.CREATE)
        audit_logger.log(AuditAction.UPDATE)
        audit_logger.log(AuditAction.DELETE)

        # Query with since
        events = audit_logger.query(since=now)
        assert len(events) == 3

        # Query with until far in future
        future = now + timedelta(days=1)
        events = audit_logger.query(until=future)
        assert len(events) == 3

    def test_query_with_limit(self, audit_logger: AuditLogger):
        """Test query with limit."""
        for _ in range(10):
            audit_logger.log(AuditAction.CREATE)

        events = audit_logger.query(limit=5)
        assert len(events) == 5

    def test_query_with_offset(self, audit_logger: AuditLogger):
        """Test query with offset."""
        for i in range(5):
            audit_logger.log(AuditAction.CREATE, details={"index": i})

        events = audit_logger.query(limit=2, offset=2)
        assert len(events) == 2

    def test_query_combined_filters(self, audit_logger: AuditLogger):
        """Test query with multiple filters."""
        audit_logger.log(AuditAction.CREATE, user="admin", job_id=1)
        audit_logger.log(AuditAction.CREATE, user="user1", job_id=1)
        audit_logger.log(AuditAction.UPDATE, user="admin", job_id=2)

        events = audit_logger.query(action=AuditAction.CREATE, user="admin", job_id=1)
        assert len(events) == 1

    def test_get_event_by_id(self, audit_logger: AuditLogger):
        """Test getting specific event by ID."""
        event = audit_logger.log(AuditAction.CREATE)

        fetched = audit_logger.get_event(event.id)
        assert fetched is not None
        assert fetched.id == event.id
        assert fetched.action == event.action

    def test_get_event_not_found(self, audit_logger: AuditLogger):
        """Test getting non-existent event."""
        fetched = audit_logger.get_event(999)
        assert fetched is None


# =============================================================================
# AuditLogger Integrity Tests
# =============================================================================


class TestAuditLoggerIntegrity:
    """Tests for integrity verification."""

    def test_verify_integrity_empty_log(self, audit_logger: AuditLogger):
        """Test integrity check on empty log."""
        is_valid, tampered = audit_logger.verify_integrity()
        assert is_valid is True
        assert tampered == []

    def test_verify_integrity_valid_chain(self, audit_logger: AuditLogger):
        """Test integrity check on valid chain."""
        for _ in range(5):
            audit_logger.log(AuditAction.CREATE)

        is_valid, tampered = audit_logger.verify_integrity()
        assert is_valid is True
        assert tampered == []

    def test_verify_integrity_detects_tampering(
        self, audit_logger: AuditLogger, temp_audit_path: Path
    ):
        """Test that tampering is detected."""
        # Create events
        for _ in range(3):
            audit_logger.log(AuditAction.CREATE)
        audit_logger.close()

        # Tamper with database
        conn = sqlite3.connect(str(temp_audit_path))
        conn.execute("UPDATE audit_events SET user = 'hacker' WHERE id = 2")
        conn.commit()
        conn.close()

        # Reopen and verify
        logger = AuditLogger(temp_audit_path)
        is_valid, tampered = logger.verify_integrity()
        assert is_valid is False
        assert 2 in tampered
        logger.close()

    def test_assert_integrity_passes(self, audit_logger: AuditLogger):
        """Test assert_integrity on valid chain."""
        audit_logger.log(AuditAction.CREATE)
        # Should not raise
        audit_logger.assert_integrity()

    def test_assert_integrity_raises_on_tampering(
        self, audit_logger: AuditLogger, temp_audit_path: Path
    ):
        """Test assert_integrity raises on tampering."""
        audit_logger.log(AuditAction.CREATE)
        audit_logger.close()

        # Tamper with database
        conn = sqlite3.connect(str(temp_audit_path))
        conn.execute("UPDATE audit_events SET user = 'hacker' WHERE id = 1")
        conn.commit()
        conn.close()

        # Reopen and assert
        logger = AuditLogger(temp_audit_path)
        with pytest.raises(TamperDetectionError):
            logger.assert_integrity()
        logger.close()


# =============================================================================
# AuditLogger Stats Tests
# =============================================================================


class TestAuditLoggerStats:
    """Tests for statistics functionality."""

    def test_get_stats_empty(self, audit_logger: AuditLogger):
        """Test stats for empty log."""
        stats = audit_logger.get_stats()
        assert stats["total_events"] == 0
        assert stats["by_action"] == {}
        assert stats["date_range"]["oldest"] is None
        assert stats["date_range"]["newest"] is None

    def test_get_stats_with_events(self, audit_logger: AuditLogger):
        """Test stats with events."""
        audit_logger.log(AuditAction.CREATE)
        audit_logger.log(AuditAction.CREATE)
        audit_logger.log(AuditAction.UPDATE)

        stats = audit_logger.get_stats()
        assert stats["total_events"] == 3
        assert stats["by_action"]["create"] == 2
        assert stats["by_action"]["update"] == 1
        assert stats["date_range"]["oldest"] is not None
        assert stats["date_range"]["newest"] is not None


# =============================================================================
# AuditLogger Export Tests
# =============================================================================


class TestAuditLoggerExport:
    """Tests for export functionality."""

    def test_export_json(self, audit_logger: AuditLogger, tmp_path: Path):
        """Test JSON export."""
        audit_logger.log(AuditAction.CREATE, user="admin")
        audit_logger.log(AuditAction.UPDATE, user="user1")

        output_path = tmp_path / "audit.json"
        count = audit_logger.export_json(str(output_path))

        assert count == 2
        assert output_path.exists()

        # Verify content
        with open(output_path) as f:
            data = json.load(f)
        assert data["event_count"] == 2
        assert len(data["events"]) == 2

    def test_export_json_with_date_filter(self, audit_logger: AuditLogger, tmp_path: Path):
        """Test JSON export with date filter."""
        now = datetime.utcnow()

        audit_logger.log(AuditAction.CREATE)

        output_path = tmp_path / "audit.json"
        # Export with future since - should get nothing
        count = audit_logger.export_json(str(output_path), since=now + timedelta(hours=1))
        assert count == 0

    def test_export_csv(self, audit_logger: AuditLogger, tmp_path: Path):
        """Test CSV export."""
        audit_logger.log(AuditAction.CREATE, user="admin", job_id=123)

        output_path = tmp_path / "audit.csv"
        count = audit_logger.export_csv(str(output_path))

        assert count == 1
        assert output_path.exists()

        # Verify content
        content = output_path.read_text()
        assert "id,timestamp,action,user,job_id" in content
        assert "create" in content
        assert "admin" in content


# =============================================================================
# RetentionPolicy Tests
# =============================================================================


class TestRetentionPolicy:
    """Tests for retention policy functionality."""

    def test_retention_policy_defaults(self):
        """Test default retention policy."""
        policy = RetentionPolicy()
        assert policy.max_age_days is None
        assert policy.max_entries is None
        assert policy.archive_path is None

    def test_retention_policy_with_values(self, tmp_path: Path):
        """Test retention policy with values."""
        archive_path = tmp_path / "archive.db"
        policy = RetentionPolicy(
            max_age_days=30,
            max_entries=1000,
            archive_path=archive_path,
        )
        assert policy.max_age_days == 30
        assert policy.max_entries == 1000
        assert policy.archive_path == archive_path

    def test_retention_policy_deletes_old_events(self, temp_audit_path: Path):
        """Test that old events are deleted."""
        policy = RetentionPolicy(max_age_days=1)
        logger = AuditLogger(temp_audit_path, retention_policy=policy)

        # Add event
        logger.log(AuditAction.CREATE)
        logger.close()

        # Manually update timestamp to be old
        conn = sqlite3.connect(str(temp_audit_path))
        old_time = (datetime.utcnow() - timedelta(days=2)).isoformat()
        conn.execute("UPDATE audit_events SET timestamp = ?", (old_time,))
        conn.commit()
        conn.close()

        # Reopen and log a new event to trigger retention check
        logger = AuditLogger(temp_audit_path, retention_policy=policy)
        logger.log(AuditAction.UPDATE)  # Trigger retention check
        events = logger.query()
        assert len(events) == 1  # Only the new UPDATE event remains
        assert events[0].action == AuditAction.UPDATE
        logger.close()

    def test_retention_policy_archives_old_events(self, temp_audit_path: Path, tmp_path: Path):
        """Test that old events are archived."""
        archive_path = tmp_path / "archive.db"
        policy = RetentionPolicy(
            max_age_days=1,
            archive_path=archive_path,
        )
        logger = AuditLogger(temp_audit_path, retention_policy=policy)

        # Add event
        logger.log(AuditAction.CREATE)
        logger.close()

        # Manually update timestamp to be old
        conn = sqlite3.connect(str(temp_audit_path))
        old_time = (datetime.utcnow() - timedelta(days=2)).isoformat()
        conn.execute("UPDATE audit_events SET timestamp = ?", (old_time,))
        conn.commit()
        conn.close()

        # Reopen - should archive old event
        logger = AuditLogger(temp_audit_path, retention_policy=policy)
        logger.log(AuditAction.UPDATE)  # Trigger retention check
        logger.close()

        # Verify archive
        assert archive_path.exists()
        conn = sqlite3.connect(str(archive_path))
        cursor = conn.execute("SELECT COUNT(*) FROM audit_events")
        count = cursor.fetchone()[0]
        assert count == 1
        conn.close()

    def test_retention_policy_max_entries(self, temp_audit_path: Path):
        """Test max entries retention."""
        policy = RetentionPolicy(max_entries=3)
        logger = AuditLogger(temp_audit_path, retention_policy=policy)

        # Add more than max events
        for _ in range(5):
            logger.log(AuditAction.CREATE)

        events = logger.query()
        # Should only have 3 most recent
        assert len(events) == 3
        logger.close()


# =============================================================================
# Global Logger Tests
# =============================================================================


class TestGlobalLogger:
    """Tests for global audit logger functionality."""

    def test_configure_global_audit_logger(self, temp_audit_path: Path):
        """Test configuring global logger."""
        logger = configure_global_audit_logger(temp_audit_path)
        assert logger is not None
        assert get_global_audit_logger() is logger

    def test_log_audit_without_global_logger(self):
        """Test log_audit without global logger."""
        # Ensure no global logger
        import oclawma.audit as audit_module

        original = audit_module._global_audit_logger
        audit_module._global_audit_logger = None

        try:
            result = log_audit(AuditAction.CREATE)
            assert result is None
        finally:
            audit_module._global_audit_logger = original

    def test_log_audit_with_global_logger(self, temp_audit_path: Path):
        """Test log_audit with global logger."""
        configure_global_audit_logger(temp_audit_path)

        event = log_audit(AuditAction.CREATE, user="admin", job_id=123)

        assert event is not None
        assert event.action == AuditAction.CREATE
        assert event.user == "admin"
        assert event.job_id == 123


# =============================================================================
# Context Manager Tests
# =============================================================================


class TestContextManager:
    """Tests for context manager usage."""

    def test_context_manager(self, temp_audit_path: Path):
        """Test using audit logger as context manager."""
        with AuditLogger(temp_audit_path) as logger:
            event = logger.log(AuditAction.CREATE)
            assert event.id is not None

        # After exit, connection should be closed
        assert logger._conn is None


# =============================================================================
# Error Handling Tests
# =============================================================================


class TestErrorHandling:
    """Tests for error handling."""

    def test_invalid_database_path(self, tmp_path: Path):
        """Test with invalid database path."""
        # Try to create logger in non-existent directory without permission
        invalid_path = "/nonexistent_dir/audit.db"
        with pytest.raises((OSError, sqlite3.Error)):
            AuditLogger(invalid_path)
