"""Tests for S3 sync functionality using moto for AWS mocking."""

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

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)


@pytest.fixture
def s3_sync_with_prefix(s3_bucket):
    """Create an S3Sync instance with a prefix."""
    return S3Sync(bucket=BUCKET_NAME, prefix="data/v1", region_name=REGION)


class TestS3SyncInit:
    """Tests for S3Sync initialization."""

    def test_init_basic(self, s3_bucket):
        """Test basic initialization."""
        sync = S3Sync(bucket=BUCKET_NAME, region_name=REGION)
        assert sync.bucket == BUCKET_NAME
        assert sync.prefix == ""

    def test_init_with_prefix(self, s3_bucket):
        """Test initialization with prefix."""
        sync = S3Sync(bucket=BUCKET_NAME, prefix="my/prefix/", region_name=REGION)
        assert sync.prefix == "my/prefix"  # Trailing slash removed

    def test_init_with_empty_prefix(self, s3_bucket):
        """Test initialization with empty prefix."""
        sync = S3Sync(bucket=BUCKET_NAME, prefix="", region_name=REGION)
        assert sync.prefix == ""


class TestUploadObject:
    """Tests for uploading single objects."""

    def test_upload_single_object(self, s3_sync, s3_bucket):
        """Test uploading a single object."""
        with Pit("test_pit", object_types=[Person]) as pit:
            stored = pit.put(Person(name="Alice", age=30))

            s3_sync.upload_object(pit, stored)

            # Verify object exists in S3
            key = f"test_pit/objects/{stored.id}.json"
            response = s3_bucket.get_object(Bucket=BUCKET_NAME, Key=key)
            assert response is not None

    def test_upload_object_with_prefix(self, s3_sync_with_prefix, s3_bucket):
        """Test uploading object with S3 prefix."""
        with Pit("test_pit", object_types=[Person]) as pit:
            stored = pit.put(Person(name="Bob", age=25))

            s3_sync_with_prefix.upload_object(pit, stored)

            # Verify key includes prefix
            key = f"data/v1/test_pit/objects/{stored.id}.json"
            response = s3_bucket.get_object(Bucket=BUCKET_NAME, Key=key)
            assert response is not None

    def test_upload_object_with_tags(self, s3_sync, s3_bucket):
        """Test uploading object with tags."""
        with Pit("test_pit", object_types=[Person]) as pit:
            stored = pit.put(Person(name="Charlie", age=35), tags=["vip", "active"])

            s3_sync.upload_object(pit, stored)

            # Verify tags are in uploaded data
            import json

            key = f"test_pit/objects/{stored.id}.json"
            response = s3_bucket.get_object(Bucket=BUCKET_NAME, Key=key)
            data = json.loads(response["Body"].read().decode("utf-8"))
            assert "vip" in data["tags"]
            assert "active" in data["tags"]


class TestDownloadObject:
    """Tests for downloading single objects."""

    def test_download_existing_object(self, s3_sync, s3_bucket):
        """Test downloading an existing object."""
        with Pit("test_pit", object_types=[Person]) as pit:
            stored = pit.put(Person(name="Diana", age=28))
            s3_sync.upload_object(pit, stored)

            # Download and verify
            data = s3_sync.download_object("test_pit", stored.id)
            assert data is not None
            assert data["data"]["name"] == "Diana"
            assert data["data"]["age"] == 28

    def test_download_nonexistent_object(self, s3_sync):
        """Test downloading a non-existent object returns None."""
        data = s3_sync.download_object("test_pit", "nonexistent-id")
        assert data is None


class TestDeleteObject:
    """Tests for deleting objects from S3."""

    def test_delete_object(self, s3_sync, s3_bucket):
        """Test deleting an object from S3."""
        with Pit("test_pit", object_types=[Person]) as pit:
            stored = pit.put(Person(name="Eve", age=22))
            s3_sync.upload_object(pit, stored)

            # Delete and verify
            s3_sync.delete_object("test_pit", stored.id)

            data = s3_sync.download_object("test_pit", stored.id)
            assert data is None


class TestUploadPit:
    """Tests for uploading entire pits."""

    def test_upload_empty_pit(self, s3_sync):
        """Test uploading an empty pit."""
        with Pit("empty_pit") as pit:
            count = s3_sync.upload_pit(pit)
            assert count == 0

    def test_upload_pit_with_objects(self, s3_sync, s3_bucket):
        """Test uploading a pit with multiple objects."""
        with Pit("test_pit", object_types=[Person]) as pit:
            pit.put(Person(name="Alice", age=30))
            pit.put(Person(name="Bob", age=25))
            pit.put(Person(name="Charlie", age=35))

            count = s3_sync.upload_pit(pit)
            assert count == 3

            # Verify metadata
            import json

            metadata_key = "test_pit/metadata.json"
            response = s3_bucket.get_object(Bucket=BUCKET_NAME, Key=metadata_key)
            metadata = json.loads(response["Body"].read().decode("utf-8"))
            assert metadata["name"] == "test_pit"
            assert metadata["object_count"] == 3

    def test_upload_pit_with_multiple_types(self, s3_sync):
        """Test uploading a pit with multiple object types."""
        with Pit("mixed_pit", object_types=[Person, Task]) as pit:
            pit.put(Person(name="Alice", age=30))
            pit.put(Task(title="Buy groceries"))
            pit.put(Task(title="Clean house", done=True))

            count = s3_sync.upload_pit(pit)
            assert count == 3


class TestListObjects:
    """Tests for listing objects in S3."""

    def test_list_objects_empty(self, s3_sync):
        """Test listing objects from empty pit."""
        object_ids = s3_sync.list_objects("empty_pit")
        assert object_ids == []

    def test_list_objects_with_data(self, s3_sync):
        """Test listing objects from pit with data."""
        with Pit("test_pit", object_types=[Person]) as pit:
            stored1 = pit.put(Person(name="Alice", age=30))
            stored2 = pit.put(Person(name="Bob", age=25))
            s3_sync.upload_pit(pit)

            object_ids = s3_sync.list_objects("test_pit")
            assert len(object_ids) == 2
            assert stored1.id in object_ids
            assert stored2.id in object_ids


class TestDownloadPit:
    """Tests for downloading entire pits."""

    def test_download_empty_pit(self, s3_sync):
        """Test downloading an empty/nonexistent pit."""
        pit = s3_sync.download_pit("nonexistent_pit", object_types=[Person])
        assert pit.count() == 0
        pit.close()

    def test_download_pit_with_objects(self, s3_sync):
        """Test downloading a pit with objects."""
        # First upload some data
        with Pit("test_pit", object_types=[Person]) as pit:
            pit.put(Person(name="Alice", age=30))
            pit.put(Person(name="Bob", age=25))
            s3_sync.upload_pit(pit)

        # Download to new pit
        downloaded_pit = s3_sync.download_pit("test_pit", object_types=[Person])
        try:
            assert downloaded_pit.count() == 2
            objects = downloaded_pit.all()
            names = {obj.name for obj in objects}
            assert names == {"Alice", "Bob"}
        finally:
            downloaded_pit.close()

    def test_download_pit_with_multiple_types(self, s3_sync):
        """Test downloading pit with multiple object types."""
        with Pit("mixed_pit", object_types=[Person, Task]) as pit:
            pit.put(Person(name="Alice", age=30))
            pit.put(Task(title="Buy groceries"))
            s3_sync.upload_pit(pit)

        downloaded = s3_sync.download_pit("mixed_pit", object_types=[Person, Task])
        try:
            assert downloaded.count() == 2
        finally:
            downloaded.close()


class TestSync:
    """Tests for bidirectional sync."""

    def test_sync_local_only(self, s3_sync):
        """Test sync uploads local-only objects."""
        with Pit("test_pit", object_types=[Person]) as pit:
            pit.put(Person(name="Alice", age=30))
            pit.put(Person(name="Bob", age=25))

            stats = s3_sync.sync(pit)

            assert stats["uploaded"] == 2
            assert stats["downloaded"] == 0
            assert stats["conflicts_resolved"] == 0

    def test_sync_remote_only(self, s3_sync):
        """Test sync downloads remote-only objects."""
        # Upload to S3 first
        with Pit("test_pit", object_types=[Person]) as upload_pit:
            upload_pit.put(Person(name="Alice", age=30))
            upload_pit.put(Person(name="Bob", age=25))
            s3_sync.upload_pit(upload_pit)

        # Sync to empty local pit
        with Pit("test_pit", object_types=[Person]) as local_pit:
            stats = s3_sync.sync(local_pit)

            assert stats["downloaded"] == 2
            assert stats["uploaded"] == 0
            assert local_pit.count() == 2

    def test_sync_mixed(self, s3_sync):
        """Test sync with both local and remote objects."""
        # Upload one object to S3
        with Pit("test_pit", object_types=[Person]) as upload_pit:
            upload_pit.put(Person(name="Alice", age=30))
            s3_sync.upload_pit(upload_pit)

        # Create local pit with different object
        with Pit("test_pit", object_types=[Person]) as local_pit:
            local_pit.put(Person(name="Bob", age=25))

            stats = s3_sync.sync(local_pit)

            # Bob uploaded, Alice downloaded
            assert stats["uploaded"] == 1
            assert stats["downloaded"] == 1
            assert local_pit.count() == 2


class TestDeletePit:
    """Tests for deleting pits from S3."""

    def test_delete_pit(self, s3_sync):
        """Test deleting all objects for a pit."""
        with Pit("test_pit", object_types=[Person]) as pit:
            pit.put(Person(name="Alice", age=30))
            pit.put(Person(name="Bob", age=25))
            s3_sync.upload_pit(pit)

        deleted = s3_sync.delete_pit("test_pit")
        # 2 objects + 1 metadata = 3 keys
        assert deleted == 3

        # Verify nothing remains
        object_ids = s3_sync.list_objects("test_pit")
        assert object_ids == []

    def test_delete_nonexistent_pit(self, s3_sync):
        """Test deleting a non-existent pit."""
        deleted = s3_sync.delete_pit("nonexistent_pit")
        assert deleted == 0


class TestPrefixHandling:
    """Tests for S3 key prefix handling."""

    def test_prefix_in_all_operations(self, s3_sync_with_prefix, s3_bucket):
        """Test that prefix is correctly used in all operations."""
        with Pit("test_pit", object_types=[Person]) as pit:
            pit.put(Person(name="Alice", age=30))
            s3_sync_with_prefix.upload_pit(pit)

        # List should work with prefix
        object_ids = s3_sync_with_prefix.list_objects("test_pit")
        assert len(object_ids) == 1

        # Download should work with prefix
        downloaded = s3_sync_with_prefix.download_pit("test_pit", object_types=[Person])
        try:
            assert downloaded.count() == 1
        finally:
            downloaded.close()

        # Delete should work with prefix
        deleted = s3_sync_with_prefix.delete_pit("test_pit")
        assert deleted == 2  # 1 object + metadata
