"""Tests for automatic S3 sync with multi-writer support."""

import threading
import time
from unittest.mock import patch

import boto3
import pytest
from moto import mock_aws
from pydantic import BaseModel

from mosh.auto_sync import AutoSyncPit, SyncConfig, SyncResult
from mosh.pit import Pit
from mosh.sync import S3Sync


class Person(BaseModel):
    """Test model for sync tests."""

    name: str
    age: int


class Task(BaseModel):
    """Another test model for sync tests."""

    title: str
    done: bool = False


BUCKET_NAME = "test-bucket"
REGION = "us-east-1"


@pytest.fixture
def aws_credentials():
    """Mocked AWS credentials for moto."""
    import os

    os.environ["AWS_ACCESS_KEY_ID"] = "testing"
    os.environ["AWS_SECRET_ACCESS_KEY"] = "testing"
    os.environ["AWS_SECURITY_TOKEN"] = "testing"
    os.environ["AWS_SESSION_TOKEN"] = "testing"
    os.environ["AWS_DEFAULT_REGION"] = REGION


@pytest.fixture
def s3_bucket(aws_credentials):
    """Create a mock S3 bucket."""
    with mock_aws():
        s3 = boto3.client("s3", region_name=REGION)
        s3.create_bucket(Bucket=BUCKET_NAME)
        yield s3


@pytest.fixture
def s3_sync(s3_bucket):
    """Create an S3Sync instance with mocked S3."""
    return S3Sync(bucket=BUCKET_NAME, region_name=REGION)


class TestSyncConfig:
    """Tests for SyncConfig defaults and customization."""

    def test_default_values(self):
        """Test SyncConfig default values."""
        config = SyncConfig()
        assert config.enabled is True
        assert config.interval_seconds == 60.0
        assert config.sync_on_close is True
        assert config.max_retries == 3
        assert config.retry_delay_seconds == 5.0

    def test_custom_values(self):
        """Test SyncConfig with custom values."""
        config = SyncConfig(
            enabled=False,
            interval_seconds=30.0,
            sync_on_close=False,
            max_retries=5,
            retry_delay_seconds=1.0,
        )
        assert config.enabled is False
        assert config.interval_seconds == 30.0
        assert config.sync_on_close is False
        assert config.max_retries == 5
        assert config.retry_delay_seconds == 1.0


class TestSyncResult:
    """Tests for SyncResult dataclass."""

    def test_successful_result(self):
        """Test creating a successful SyncResult."""
        result = SyncResult(
            success=True,
            stats={"uploaded": 5, "downloaded": 3, "conflicts_resolved": 1},
            retries=0,
        )
        assert result.success is True
        assert result.stats["uploaded"] == 5
        assert result.error is None

    def test_failed_result(self):
        """Test creating a failed SyncResult."""
        error = RuntimeError("Network error")
        result = SyncResult(success=False, error=error, retries=3)
        assert result.success is False
        assert result.error is error
        assert result.retries == 3


class TestAutoSyncPitInit:
    """Tests for AutoSyncPit initialization."""

    def test_basic_init(self, s3_sync):
        """Test basic initialization."""
        config = SyncConfig(enabled=False)
        pit = AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        )
        try:
            assert pit.name == "test_pit"
            assert pit.db_path is None
            assert pit.id_field is None
        finally:
            pit.close()

    def test_init_with_db_path(self, s3_sync, tmp_path):
        """Test initialization with database path."""
        db_path = str(tmp_path / "test.db")
        config = SyncConfig(enabled=False)
        pit = AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            db_path=db_path,
            object_types=[Person],
            config=config,
        )
        try:
            assert pit.db_path == db_path
        finally:
            pit.close()

    def test_init_with_id_field(self, s3_sync):
        """Test initialization with id_field."""
        config = SyncConfig(enabled=False)
        pit = AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            id_field="name",
            config=config,
        )
        try:
            assert pit.id_field == "name"
        finally:
            pit.close()

    def test_auto_sync_starts_by_default(self, s3_sync):
        """Test that auto-sync starts by default."""
        config = SyncConfig(interval_seconds=60.0)
        pit = AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        )
        try:
            assert pit._worker is not None
            assert pit._worker.is_alive()
        finally:
            pit.close()

    def test_auto_sync_disabled(self, s3_sync):
        """Test that auto-sync can be disabled."""
        config = SyncConfig(enabled=False)
        pit = AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        )
        try:
            assert pit._worker is None
        finally:
            pit.close()


class TestAutoSyncPitCRUD:
    """Tests for thread-safe CRUD operations."""

    def test_put_and_get(self, s3_sync):
        """Test put and get operations."""
        config = SyncConfig(enabled=False)
        with AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        ) as pit:
            stored = pit.put(Person(name="Alice", age=30))
            assert stored.obj.name == "Alice"

            retrieved = pit.get(stored.id)
            assert retrieved is not None
            assert retrieved.name == "Alice"

    def test_get_stored(self, s3_sync):
        """Test get_stored returns StoredObject with metadata."""
        config = SyncConfig(enabled=False)
        with AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        ) as pit:
            stored = pit.put(Person(name="Bob", age=25))
            retrieved = pit.get_stored(stored.id)
            assert retrieved is not None
            assert retrieved.obj.name == "Bob"
            assert retrieved.created_at is not None

    def test_delete(self, s3_sync):
        """Test delete operation."""
        config = SyncConfig(enabled=False)
        with AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        ) as pit:
            stored = pit.put(Person(name="Charlie", age=35))
            assert pit.delete(stored.id) is True
            assert pit.get(stored.id) is None

    def test_search(self, s3_sync):
        """Test search operation."""
        config = SyncConfig(enabled=False)
        with AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        ) as pit:
            pit.put(Person(name="Alice", age=30))
            pit.put(Person(name="Bob", age=25))
            pit.put(Person(name="Charlie", age=30))

            results = pit.search(age=30)
            assert len(results) == 2

    def test_search_stored(self, s3_sync):
        """Test search_stored returns StoredObjects with metadata."""
        config = SyncConfig(enabled=False)
        with AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        ) as pit:
            pit.put(Person(name="Alice", age=30))
            pit.put(Person(name="Bob", age=25))

            results = pit.search_stored(name="Alice")
            assert len(results) == 1
            assert results[0].obj.name == "Alice"
            assert results[0].created_at is not None

    def test_all_and_count(self, s3_sync):
        """Test all and count operations."""
        config = SyncConfig(enabled=False)
        with AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        ) as pit:
            pit.put(Person(name="Alice", age=30))
            pit.put(Person(name="Bob", age=25))

            assert pit.count() == 2
            assert len(pit.all()) == 2
            assert len(pit.all_stored()) == 2

    def test_clear(self, s3_sync):
        """Test clear operation."""
        config = SyncConfig(enabled=False)
        with AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        ) as pit:
            pit.put(Person(name="Alice", age=30))
            pit.put(Person(name="Bob", age=25))

            count = pit.clear()
            assert count == 2
            assert pit.count() == 0


class TestAutoSyncPitTags:
    """Tests for tag operations."""

    def test_add_and_remove_tag(self, s3_sync):
        """Test add_tag and remove_tag operations."""
        config = SyncConfig(enabled=False)
        with AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        ) as pit:
            stored = pit.put(Person(name="Alice", age=30))

            assert pit.add_tag(stored.id, "vip") is True
            tags = pit.get_tags(stored.id)
            assert "vip" in tags

            assert pit.remove_tag(stored.id, "vip") is True
            tags = pit.get_tags(stored.id)
            assert "vip" not in tags

    def test_search_tags(self, s3_sync):
        """Test search_tags operation."""
        config = SyncConfig(enabled=False)
        with AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        ) as pit:
            pit.put(Person(name="Alice", age=30), tags=["vip"])
            pit.put(Person(name="Bob", age=25), tags=["regular"])

            results = pit.search_tags(["vip"])
            assert len(results) == 1
            assert results[0].name == "Alice"


class TestAutoSyncPitContextManager:
    """Tests for context manager support."""

    def test_context_manager_closes(self, s3_sync):
        """Test that context manager closes pit properly."""
        config = SyncConfig(enabled=False, sync_on_close=False)
        pit = AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        )
        with pit:
            pit.put(Person(name="Alice", age=30))

        assert pit._closed is True

    def test_context_manager_stops_worker(self, s3_sync):
        """Test that context manager stops background worker."""
        config = SyncConfig(interval_seconds=0.1, sync_on_close=False)
        with AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        ) as pit:
            assert pit._worker is not None
            worker = pit._worker

        # Worker should be stopped after exit
        assert worker is None or not worker.is_alive()


class TestAutoSyncPitLen:
    """Tests for __len__ and __contains__."""

    def test_len(self, s3_sync):
        """Test __len__ implementation."""
        config = SyncConfig(enabled=False)
        with AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        ) as pit:
            assert len(pit) == 0
            pit.put(Person(name="Alice", age=30))
            assert len(pit) == 1

    def test_contains(self, s3_sync):
        """Test __contains__ implementation."""
        config = SyncConfig(enabled=False)
        with AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        ) as pit:
            stored = pit.put(Person(name="Alice", age=30))
            assert stored.id in pit
            assert "nonexistent" not in pit


class TestSyncNow:
    """Tests for sync_now method."""

    def test_sync_now_uploads(self, s3_sync):
        """Test sync_now uploads local objects."""
        config = SyncConfig(enabled=False, sync_on_close=False)
        with AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        ) as pit:
            pit.put(Person(name="Alice", age=30))
            pit.put(Person(name="Bob", age=25))

            result = pit.sync_now()

            assert result.success is True
            assert result.stats["uploaded"] == 2

    def test_sync_now_downloads(self, s3_sync):
        """Test sync_now downloads remote objects."""
        config = SyncConfig(enabled=False, sync_on_close=False)

        # First upload some data
        with Pit("test_pit", object_types=[Person]) as upload_pit:
            upload_pit.put(Person(name="Alice", age=30))
            s3_sync.upload_pit(upload_pit)

        # Sync to empty AutoSyncPit
        with AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        ) as pit:
            result = pit.sync_now()

            assert result.success is True
            assert result.stats["downloaded"] == 1
            assert pit.count() == 1


class TestStartStopAutoSync:
    """Tests for start_auto_sync and stop_auto_sync."""

    def test_start_auto_sync(self, s3_sync):
        """Test starting auto-sync."""
        config = SyncConfig(enabled=False)
        pit = AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        )
        try:
            assert pit._worker is None

            pit.start_auto_sync()

            assert pit._worker is not None
            assert pit._worker.is_alive()
        finally:
            pit.close()

    def test_stop_auto_sync(self, s3_sync):
        """Test stopping auto-sync."""
        config = SyncConfig(interval_seconds=60.0)
        pit = AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        )
        try:
            assert pit._worker is not None

            pit.stop_auto_sync()

            assert pit._worker is None
        finally:
            pit.close()

    def test_start_auto_sync_idempotent(self, s3_sync):
        """Test that starting auto-sync twice doesn't create multiple workers."""
        config = SyncConfig(interval_seconds=60.0)
        pit = AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        )
        try:
            worker1 = pit._worker

            pit.start_auto_sync()

            assert pit._worker is worker1
        finally:
            pit.close()


class TestConcurrentWrites:
    """Tests for concurrent write operations from multiple threads."""

    def test_concurrent_puts(self, s3_sync):
        """Test concurrent put operations from multiple threads."""
        config = SyncConfig(enabled=False)
        pit = AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        )
        errors = []
        num_threads = 10
        items_per_thread = 20

        def writer(thread_id: int):
            try:
                for i in range(items_per_thread):
                    pit.put(Person(name=f"Person_{thread_id}_{i}", age=thread_id))
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=writer, args=(i,)) for i in range(num_threads)]

        for t in threads:
            t.start()
        for t in threads:
            t.join()

        pit.close()

        assert len(errors) == 0
        # Each thread writes items_per_thread items
        # Total should be num_threads * items_per_thread = 200

    def test_concurrent_reads_and_writes(self, s3_sync):
        """Test concurrent read and write operations."""
        config = SyncConfig(enabled=False)
        pit = AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        )
        errors = []
        stored_ids = []
        lock = threading.Lock()

        # Pre-populate with some data
        for i in range(50):
            stored = pit.put(Person(name=f"Initial_{i}", age=i))
            stored_ids.append(stored.id)

        def reader():
            try:
                for _ in range(100):
                    pit.all()
                    pit.count()
                    if stored_ids:
                        pit.get(stored_ids[0])
            except Exception as e:
                errors.append(e)

        def writer():
            try:
                for i in range(50):
                    stored = pit.put(Person(name=f"New_{i}", age=100 + i))
                    with lock:
                        stored_ids.append(stored.id)
            except Exception as e:
                errors.append(e)

        threads = [
            threading.Thread(target=reader),
            threading.Thread(target=reader),
            threading.Thread(target=writer),
            threading.Thread(target=writer),
        ]

        for t in threads:
            t.start()
        for t in threads:
            t.join()

        pit.close()

        assert len(errors) == 0


class TestBackgroundSyncExecution:
    """Tests for background sync execution."""

    def test_background_sync_runs(self, s3_sync):
        """Test that background sync runs at configured interval."""
        sync_count = []

        def on_complete(result):
            sync_count.append(result)

        config = SyncConfig(
            interval_seconds=0.1,
            sync_on_close=False,
        )
        pit = AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
            on_sync_complete=on_complete,
        )

        pit.put(Person(name="Alice", age=30))

        # Wait for a few sync cycles
        time.sleep(0.5)

        pit.close()

        # Should have synced multiple times
        assert len(sync_count) >= 2


class TestSyncOnClose:
    """Tests for sync_on_close behavior."""

    def test_sync_on_close_enabled(self, s3_sync):
        """Test that sync runs on close when enabled."""
        config = SyncConfig(enabled=False, sync_on_close=True)
        pit = AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        )

        pit.put(Person(name="Alice", age=30))
        pit.close()

        # Verify data was synced to S3
        object_ids = s3_sync.list_objects("test_pit")
        assert len(object_ids) == 1

    def test_sync_on_close_disabled(self, s3_sync):
        """Test that sync doesn't run on close when disabled."""
        config = SyncConfig(enabled=False, sync_on_close=False)
        pit = AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        )

        pit.put(Person(name="Alice", age=30))
        pit.close()

        # Verify no data was synced to S3
        object_ids = s3_sync.list_objects("test_pit")
        assert len(object_ids) == 0


class TestSyncRetryLogic:
    """Tests for sync retry logic."""

    def test_sync_now_retries_on_failure(self, s3_sync):
        """Test that sync_now retries on transient failures."""
        config = SyncConfig(
            enabled=False,
            max_retries=3,
            retry_delay_seconds=0.01,
        )
        pit = AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        )

        attempt_count = [0]
        original_sync = s3_sync.sync

        def failing_sync(p):
            attempt_count[0] += 1
            if attempt_count[0] < 3:
                raise RuntimeError("Transient error")
            return original_sync(p)

        pit.put(Person(name="Alice", age=30))

        with patch.object(s3_sync, "sync", side_effect=failing_sync):
            result = pit.sync_now()

        pit.close()

        assert result.success is True
        assert result.retries == 2
        assert attempt_count[0] == 3

    def test_sync_now_fails_after_max_retries(self, s3_sync):
        """Test that sync_now fails after max retries exceeded."""
        config = SyncConfig(
            enabled=False,
            max_retries=3,
            retry_delay_seconds=0.01,
        )
        pit = AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        )

        pit.put(Person(name="Alice", age=30))

        def always_fail(p):
            raise RuntimeError("Persistent error")

        with patch.object(s3_sync, "sync", side_effect=always_fail):
            result = pit.sync_now()

        pit.close()

        assert result.success is False
        assert result.retries == 3
        assert isinstance(result.error, RuntimeError)


class TestSyncWithRetry:
    """Tests for S3Sync.sync_with_retry method."""

    def test_sync_with_retry_success(self, s3_sync):
        """Test successful sync with retry."""
        with Pit("test_pit", object_types=[Person]) as pit:
            pit.put(Person(name="Alice", age=30))

            stats = s3_sync.sync_with_retry(pit)

            assert stats["uploaded"] == 1

    def test_sync_with_retry_retries_on_failure(self, s3_sync):
        """Test sync_with_retry retries on failure."""
        attempt_count = [0]
        original_sync = s3_sync.sync

        def failing_sync(p):
            attempt_count[0] += 1
            if attempt_count[0] < 2:
                raise RuntimeError("Transient error")
            return original_sync(p)

        with Pit("test_pit", object_types=[Person]) as pit:
            pit.put(Person(name="Alice", age=30))

            with patch.object(s3_sync, "sync", side_effect=failing_sync):
                stats = s3_sync.sync_with_retry(pit, max_retries=3, retry_delay=0.01)

            assert stats["uploaded"] == 1
            assert attempt_count[0] == 2

    def test_sync_with_retry_raises_after_max(self, s3_sync):
        """Test sync_with_retry raises after max retries."""

        def always_fail(p):
            raise RuntimeError("Persistent error")

        with Pit("test_pit", object_types=[Person]) as pit:
            pit.put(Person(name="Alice", age=30))

            with patch.object(s3_sync, "sync", side_effect=always_fail):
                with pytest.raises(RuntimeError, match="Persistent error"):
                    s3_sync.sync_with_retry(pit, max_retries=3, retry_delay=0.01)


class TestCallbacks:
    """Tests for sync callbacks."""

    def test_on_sync_complete_callback(self, s3_sync):
        """Test on_sync_complete callback is called."""
        results = []

        def on_complete(result):
            results.append(result)

        config = SyncConfig(
            interval_seconds=0.1,
            sync_on_close=False,
        )
        pit = AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
            on_sync_complete=on_complete,
        )

        pit.put(Person(name="Alice", age=30))
        time.sleep(0.3)
        pit.close()

        assert len(results) >= 1
        assert results[0].success is True

    def test_on_sync_error_callback(self, s3_sync):
        """Test on_sync_error callback is called on failure."""
        errors = []

        def on_error(error):
            errors.append(error)

        def always_fail(p):
            raise RuntimeError("Sync failed")

        config = SyncConfig(
            interval_seconds=0.1,
            max_retries=1,
            retry_delay_seconds=0.01,
            sync_on_close=False,
        )
        pit = AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
            on_sync_error=on_error,
        )

        pit.put(Person(name="Alice", age=30))

        with patch.object(s3_sync, "sync", side_effect=always_fail):
            time.sleep(0.3)

        pit.close()

        assert len(errors) >= 1


class TestRegisterType:
    """Tests for register_type method."""

    def test_register_type(self, s3_sync):
        """Test registering a new type."""
        config = SyncConfig(enabled=False)
        with AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            config=config,
        ) as pit:
            pit.register_type(Person)
            stored = pit.put(Person(name="Alice", age=30))

            retrieved = pit.get(stored.id)
            assert retrieved is not None
            assert isinstance(retrieved, Person)


class TestGracefulShutdown:
    """Tests for graceful shutdown behavior."""

    def test_close_is_idempotent(self, s3_sync):
        """Test that close can be called multiple times."""
        config = SyncConfig(enabled=False, sync_on_close=False)
        pit = AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        )

        pit.close()
        pit.close()  # Should not raise

    def test_worker_stops_on_close(self, s3_sync):
        """Test that worker thread stops on close."""
        config = SyncConfig(interval_seconds=60.0)
        pit = AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        )

        worker = pit._worker
        assert worker is not None
        assert worker.is_alive()

        pit.close()

        # Give worker time to terminate
        time.sleep(0.1)
        assert not worker.is_alive()


class TestDuckDBConfigIntegration:
    """Tests for DuckDB configuration with AutoSyncPit."""

    def test_default_duckdb_config(self, s3_sync):
        """Test that default DuckDBConfig is applied."""
        config = SyncConfig(enabled=False)
        with AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=config,
        ) as pit:
            assert pit.duckdb_config is not None
            assert pit.duckdb_config.memory_limit is None
            assert pit.duckdb_config.threads is None

    def test_custom_duckdb_config(self, s3_sync):
        """Test passing custom DuckDB configuration."""
        from mosh.pit import DuckDBConfig

        duckdb_config = DuckDBConfig(memory_limit="256MB", threads=2)
        sync_config = SyncConfig(enabled=False)

        with AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=sync_config,
            duckdb_config=duckdb_config,
        ) as pit:
            assert pit.duckdb_config.memory_limit == "256MB"
            assert pit.duckdb_config.threads == 2

            # Verify operations work
            pit.put(Person(name="Alice", age=30))
            assert pit.count() == 1

    def test_duckdb_config_with_sync(self, s3_sync):
        """Test DuckDB config works with sync operations."""
        from mosh.pit import DuckDBConfig

        duckdb_config = DuckDBConfig(memory_limit="128MB")
        sync_config = SyncConfig(enabled=False, sync_on_close=False)

        with AutoSyncPit(
            name="test_pit",
            s3_sync=s3_sync,
            object_types=[Person],
            config=sync_config,
            duckdb_config=duckdb_config,
        ) as pit:
            pit.put(Person(name="Alice", age=30))
            pit.put(Person(name="Bob", age=25))

            result = pit.sync_now()
            assert result.success is True
            assert result.stats["uploaded"] == 2
