"""
Tests for the ImmutableLedgerMixin and related mixins.
"""

import pytest
from datetime import datetime
from unittest.mock import patch

from atomsql import Database, Model
from atomsql.orm import fields

from atomingest.core.mixins import ImmutableLedgerMixin


class TestImmutableLedgerMixin:
    """Tests for ImmutableLedgerMixin class."""

    @pytest.fixture
    def db(self):
        """Create an in-memory database for testing."""
        database = Database("sqlite:///:memory:")
        yield database
        database.close()

    @pytest.fixture
    def immutable_model_class(self, db):
        """Create a test model with ImmutableLedgerMixin."""

        class TestLedger(ImmutableLedgerMixin, Model):
            transaction_id = fields.CharField(max_length=50)
            amount = fields.FloatField()
            description = fields.TextField()

        db.register(TestLedger)
        return TestLedger

    @pytest.fixture
    def mutable_model_class(self, db):
        """Create a regular model without ImmutableLedgerMixin for comparison."""

        class TestMutable(Model):
            transaction_id = fields.CharField(max_length=50)
            amount = fields.FloatField()
            description = fields.TextField()

        db.register(TestMutable)
        return TestMutable

    def test_mixin_adds_ingested_at_field(self, immutable_model_class):
        """Test that the mixin adds an ingested_at DateTimeField."""
        assert hasattr(immutable_model_class, "ingested_at")
        # Check if the class attribute exists
        assert "ingested_at" in dir(immutable_model_class)

    def test_ingested_at_has_default_value(self, immutable_model_class):
        """Test that ingested_at field has a default value function."""
        # The field is defined at the class level with a default
        field_attr = getattr(ImmutableLedgerMixin, "ingested_at", None)
        assert field_attr is not None
        assert hasattr(field_attr, "default")

    def test_new_record_can_be_saved(self, immutable_model_class):
        """Test that a new record can be saved successfully."""
        record = immutable_model_class(
            transaction_id="TXN001", amount=100.50, description="Initial deposit"
        )

        # Should not raise any exception
        record.save()

        # Verify the record was saved
        assert record.id is not None

    def test_ingested_at_is_automatically_set(self, immutable_model_class):
        """Test that ingested_at field exists and can be set."""
        record = immutable_model_class(
            transaction_id="TXN002", amount=250.00, description="Payment"
        )
        record.save()

        # The field exists and is accessible
        assert hasattr(record, "ingested_at")

    def test_update_existing_record_raises_permission_error(
        self, immutable_model_class
    ):
        """Test that updating an existing record raises PermissionError."""
        # Create and save a record
        record = immutable_model_class(
            transaction_id="TXN003", amount=500.00, description="Initial value"
        )
        record.save()

        # Attempt to update the record
        record.amount = 750.00

        with pytest.raises(PermissionError) as exc_info:
            record.save()

        assert "Immutable Ledger Violation" in str(exc_info.value)
        assert "Cannot update existing record" in str(exc_info.value)

    def test_delete_raises_permission_error(self, immutable_model_class):
        """Test that deleting a record raises PermissionError."""
        # Create and save a record
        record = immutable_model_class(
            transaction_id="TXN004", amount=1000.00, description="To be deleted"
        )
        record.save()

        # Attempt to delete the record
        with pytest.raises(PermissionError) as exc_info:
            record.delete()

        assert "Immutable Ledger Violation" in str(exc_info.value)
        assert "Cannot delete record" in str(exc_info.value)

    def test_error_message_includes_table_name(self, immutable_model_class):
        """Test that error messages include the table name."""
        record = immutable_model_class(
            transaction_id="TXN005", amount=100.00, description="Test"
        )
        record.save()

        # Try to update
        record.amount = 200.00
        with pytest.raises(PermissionError) as exc_info:
            record.save()

        error_message = str(exc_info.value)
        # Check if table name is mentioned (it might be 'unknown' or actual table name)
        assert "table" in error_message.lower()

    def test_multiple_new_records_can_be_created(self, immutable_model_class):
        """Test that multiple new records can be created successfully."""
        records = []
        for i in range(5):
            record = immutable_model_class(
                transaction_id=f"TXN{i:03d}",
                amount=100.00 * (i + 1),
                description=f"Transaction {i}",
            )
            record.save()
            records.append(record)

        # All records should have IDs
        assert all(r.id is not None for r in records)

    def test_comparison_with_mutable_model(
        self, immutable_model_class, mutable_model_class
    ):
        """Test that mutable models can be updated while immutable cannot."""
        # Create immutable record
        immutable_record = immutable_model_class(
            transaction_id="IMMUT001", amount=100.00, description="Immutable"
        )
        immutable_record.save()

        # Create mutable record
        mutable_record = mutable_model_class(
            transaction_id="MUT001", amount=100.00, description="Mutable"
        )
        mutable_record.save()

        # Mutable record should be updatable
        mutable_record.amount = 200.00
        mutable_record.save()  # Should not raise

        # Immutable record should not be updatable
        immutable_record.amount = 200.00
        with pytest.raises(PermissionError):
            immutable_record.save()

    def test_immutable_record_can_be_queried(self, immutable_model_class):
        """Test that immutable records can be queried normally."""
        # Create several records
        for i in range(3):
            record = immutable_model_class(
                transaction_id=f"QUERY{i:03d}",
                amount=100.00 * (i + 1),
                description=f"Query test {i}",
            )
            record.save()

        # Query all records
        results = list(immutable_model_class.objects())
        assert len(results) >= 3

        # Query with filter
        filtered = list(
            immutable_model_class.objects().filter(transaction_id="QUERY001")
        )
        assert len(filtered) >= 1

    def test_save_with_no_id_attribute_allows_save(self, immutable_model_class):
        """Test that records without an id attribute can be saved (new records)."""
        record = immutable_model_class(
            transaction_id="NOID001", amount=50.00, description="No ID test"
        )

        # Before save, id should be None or not set
        assert getattr(record, "id", None) is None

        # Save should succeed
        record.save()
        assert record.id is not None

    def test_save_with_existing_id_prevents_save(self, immutable_model_class):
        """Test that records with an existing id cannot be saved again."""
        record = immutable_model_class(
            transaction_id="EXISTID001", amount=75.00, description="Existing ID test"
        )
        record.save()

        # Modify the record
        record.description = "Modified description"

        # Attempt to save should fail
        with pytest.raises(PermissionError) as exc_info:
            record.save()

        assert "Cannot update existing record" in str(exc_info.value)

    def test_ingested_at_timestamp_is_immutable(self, immutable_model_class):
        """Test that the ingested_at timestamp cannot be changed after creation."""
        record = immutable_model_class(
            transaction_id="TIME001", amount=100.00, description="Timestamp test"
        )
        record.save()

        # Try to modify ingested_at (though the save will fail anyway)
        record.ingested_at = datetime(2020, 1, 1, 0, 0, 0)

        with pytest.raises(PermissionError):
            record.save()

    def test_delete_on_new_record_still_raises_error(self, immutable_model_class):
        """Test that delete raises error even on unsaved records."""
        record = immutable_model_class(
            transaction_id="DEL001", amount=100.00, description="Delete test"
        )

        # Even before saving, delete should raise error
        with pytest.raises(PermissionError) as exc_info:
            record.delete()

        assert "Cannot delete record" in str(exc_info.value)

    def test_mixin_with_multiple_inheritance(self, db):
        """Test that the mixin works correctly with multiple inheritance."""

        class AnotherMixin:
            """A simple mixin for testing multiple inheritance."""

            def custom_method(self):
                return "custom"

        class MultiInheritanceModel(ImmutableLedgerMixin, AnotherMixin, Model):
            name = fields.CharField(max_length=100)

        db.register(MultiInheritanceModel)

        # Test that both mixins work
        record = MultiInheritanceModel(name="Test")
        assert hasattr(record, "ingested_at")
        assert hasattr(record, "custom_method")
        assert record.custom_method() == "custom"

        # Test immutability still works
        record.save()
        record.name = "Modified"
        with pytest.raises(PermissionError):
            record.save()

    def test_immutable_model_preserves_data_integrity(self, immutable_model_class):
        """Test that attempting to update raises error, preventing persistence."""
        # Create a record
        record = immutable_model_class(
            transaction_id="INTEG001",
            amount=999.99,
            description="Integrity test record",
        )
        record.save()

        # Attempt to modify
        record.amount = 111.11
        record.description = "Modified"

        # Save should fail - preventing any database modification
        with pytest.raises(PermissionError) as exc_info:
            record.save()

        # Verify the error message
        assert "Cannot update existing record" in str(exc_info.value)
        assert "Immutable Ledger Violation" in str(exc_info.value)

    def test_batch_creation_performance(self, immutable_model_class):
        """Test that multiple records can be created efficiently."""
        records_count = 20

        for i in range(records_count):
            record = immutable_model_class(
                transaction_id=f"BATCH{i:04d}",
                amount=10.00 * i,
                description=f"Batch record {i}",
            )
            record.save()

        # Query to verify all were created
        all_records = list(immutable_model_class.objects())
        assert len(all_records) >= records_count

    def test_super_save_is_called_on_new_records(self, immutable_model_class):
        """Test that the parent save method is called for new records."""
        record = immutable_model_class(
            transaction_id="SUPER001", amount=50.00, description="Super test"
        )

        # Mock the parent's save to verify it's called
        with patch.object(Model, "save", return_value=None) as mock_save:
            # Set id to None to simulate new record
            record.id = None
            record.save()

            # Verify parent save was called
            mock_save.assert_called_once()

    def test_delete_always_raises_permission_error(self, immutable_model_class):
        """Test that delete always raises PermissionError."""
        record = immutable_model_class(
            transaction_id="DELSUP001", amount=50.00, description="Delete test"
        )
        record.save()

        # Attempt to delete should always raise
        with pytest.raises(PermissionError) as exc_info:
            record.delete()

        assert "Cannot delete record" in str(exc_info.value)

    def test_error_message_contains_table_reference(self, immutable_model_class):
        """Test that error messages contain table reference."""
        record = immutable_model_class(
            transaction_id="TBLNAME001", amount=100.00, description="Table name test"
        )
        record.save()

        record.amount = 200.00

        # Should raise PermissionError with table mention
        with pytest.raises(PermissionError) as exc_info:
            record.save()

        # Error should mention table
        assert "table" in str(exc_info.value).lower()
