"""Tests for Pit class."""

import os
import tempfile

from pydantic import BaseModel

from mosh import Pit, StoredObject


class Person(BaseModel):
    """Test person class."""

    name: str
    email: str
    age: int


class Product(BaseModel):
    """Test product class."""

    name: str
    price: float
    in_stock: bool = True


class TestPitCreation:
    """Tests for Pit creation."""

    def test_create_in_memory_pit(self) -> None:
        """Test creating an in-memory pit."""
        pit = Pit("test")
        assert pit.name == "test"
        assert pit.db_path is None
        pit.close()

    def test_create_file_backed_pit(self) -> None:
        """Test creating a file-backed pit."""
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = os.path.join(tmpdir, "test.duckdb")
            pit = Pit("test", db_path=db_path)
            assert pit.name == "test"
            assert pit.db_path == db_path
            pit.close()
            assert os.path.exists(db_path)

    def test_pit_context_manager(self) -> None:
        """Test using pit as context manager."""
        with Pit("test", object_types=[Person]) as pit:
            pit.put(Person(name="Test", email="test@test.com", age=25))
            assert pit.count() == 1

    def test_pit_with_id_field(self) -> None:
        """Test creating pit with id_field configuration."""
        with Pit("test", object_types=[Person], id_field="email") as pit:
            stored = pit.put(Person(name="Alice", email="alice@example.com", age=30))
            assert stored.id == "alice@example.com"

            # Get by the extracted ID
            retrieved = pit.get("alice@example.com")
            assert retrieved is not None
            assert retrieved.name == "Alice"


class TestPitCRUD:
    """Tests for CRUD operations."""

    def test_put_and_get(self) -> None:
        """Test storing and retrieving an object."""
        with Pit("test", object_types=[Person]) as pit:
            person = Person(name="Alice", email="alice@example.com", age=30)
            stored = pit.put(person)
            retrieved = pit.get(stored.id)
            assert retrieved is not None
            assert retrieved.name == "Alice"
            assert retrieved.email == "alice@example.com"
            assert retrieved.age == 30

    def test_get_stored_returns_stored_object(self) -> None:
        """Test that get_stored returns a StoredObject with metadata."""
        with Pit("test", object_types=[Person]) as pit:
            person = Person(name="Alice", email="alice@example.com", age=30)
            stored = pit.put(person)
            retrieved = pit.get_stored(stored.id)
            assert retrieved is not None
            assert isinstance(retrieved, StoredObject)
            assert retrieved.obj.name == "Alice"
            assert retrieved.created_at is not None
            assert retrieved.updated_at is not None

    def test_put_returns_stored_object(self) -> None:
        """Test that put returns a StoredObject with metadata."""
        with Pit("test", object_types=[Person]) as pit:
            person = Person(name="Alice", email="alice@example.com", age=30)
            stored = pit.put(person)
            assert isinstance(stored, StoredObject)
            assert stored.id is not None
            assert len(stored.id) == 36  # UUID
            assert stored.obj.name == "Alice"
            assert stored.created_at is not None
            assert stored.updated_at is not None

    def test_put_with_explicit_id(self) -> None:
        """Test storing with an explicit ID."""
        with Pit("test", object_types=[Person]) as pit:
            person = Person(name="Alice", email="alice@example.com", age=30)
            stored = pit.put(person, id="my-custom-id")
            assert stored.id == "my-custom-id"
            retrieved = pit.get("my-custom-id")
            assert retrieved is not None
            assert retrieved.name == "Alice"

    def test_put_with_tags(self) -> None:
        """Test storing with initial tags."""
        with Pit("test", object_types=[Person]) as pit:
            person = Person(name="Alice", email="alice@example.com", age=30)
            stored = pit.put(person, tags=["vip", "active"])
            assert stored.tags == ["vip", "active"]
            retrieved = pit.get_stored(stored.id)
            assert retrieved is not None
            assert retrieved.tags == ["vip", "active"]

    def test_put_updates_existing(self) -> None:
        """Test that put updates existing object."""
        with Pit("test", object_types=[Person]) as pit:
            person = Person(name="Alice", email="alice@example.com", age=30)
            pit.put(person, id="fixed-id")

            person2 = Person(name="Alice", email="alice@example.com", age=31)
            pit.put(person2, id="fixed-id")

            retrieved = pit.get("fixed-id")
            assert retrieved is not None
            assert retrieved.age == 31
            assert pit.count() == 1

    def test_get_nonexistent(self) -> None:
        """Test getting an object that doesn't exist."""
        with Pit("test") as pit:
            result = pit.get("nonexistent-id")
            assert result is None

    def test_get_returns_object(self) -> None:
        """Test that get returns the object directly."""
        with Pit("test", object_types=[Person]) as pit:
            person = Person(name="Alice", email="alice@example.com", age=30)
            stored = pit.put(person)

            obj = pit.get(stored.id)
            assert obj is not None
            assert isinstance(obj, Person)
            assert obj.name == "Alice"

            # Non-existent returns None
            assert pit.get("nonexistent") is None

    def test_delete(self) -> None:
        """Test deleting an object."""
        with Pit("test", object_types=[Person]) as pit:
            person = Person(name="Test", email="test@test.com", age=25)
            stored = pit.put(person)
            assert pit.count() == 1

            result = pit.delete(stored.id)
            assert result is True
            assert pit.count() == 0
            assert pit.get(stored.id) is None

    def test_delete_nonexistent(self) -> None:
        """Test deleting an object that doesn't exist."""
        with Pit("test") as pit:
            result = pit.delete("nonexistent-id")
            assert result is False

    def test_all(self) -> None:
        """Test retrieving all objects."""
        with Pit("test", object_types=[Person]) as pit:
            people = [
                Person(name="Alice", email="alice@example.com", age=30),
                Person(name="Bob", email="bob@example.com", age=25),
                Person(name="Charlie", email="charlie@example.com", age=35),
            ]
            for person in people:
                pit.put(person)

            all_objects = pit.all()
            assert len(all_objects) == 3
            names = {p.name for p in all_objects}
            assert names == {"Alice", "Bob", "Charlie"}

    def test_all_stored(self) -> None:
        """Test all_stored returns StoredObjects with metadata."""
        with Pit("test", object_types=[Person]) as pit:
            pit.put(Person(name="Alice", email="a@test.com", age=30))
            pit.put(Person(name="Bob", email="b@test.com", age=25))

            stored_objects = pit.all_stored()
            assert len(stored_objects) == 2
            assert all(isinstance(s, StoredObject) for s in stored_objects)
            assert all(s.created_at is not None for s in stored_objects)

    def test_count(self) -> None:
        """Test counting objects."""
        with Pit("test", object_types=[Person]) as pit:
            assert pit.count() == 0
            pit.put(Person(name="A", email="a@test.com", age=1))
            assert pit.count() == 1
            pit.put(Person(name="B", email="b@test.com", age=2))
            assert pit.count() == 2

    def test_clear(self) -> None:
        """Test clearing all objects."""
        with Pit("test", object_types=[Person]) as pit:
            for i in range(5):
                pit.put(Person(name=f"P{i}", email=f"p{i}@test.com", age=i))
            assert pit.count() == 5

            deleted = pit.clear()
            assert deleted == 5
            assert pit.count() == 0

    def test_contains(self) -> None:
        """Test checking if object exists."""
        with Pit("test", object_types=[Person]) as pit:
            stored = pit.put(Person(name="Test", email="t@t.com", age=1))
            assert "nonexistent" not in pit
            assert stored.id in pit

    def test_len(self) -> None:
        """Test len() on pit."""
        with Pit("test", object_types=[Person]) as pit:
            assert len(pit) == 0
            pit.put(Person(name="Test", email="t@t.com", age=1))
            assert len(pit) == 1


class TestPitSearch:
    """Tests for search functionality."""

    def test_search_by_field(self) -> None:
        """Test searching by field value."""
        with Pit("test", object_types=[Person]) as pit:
            pit.put(Person(name="Alice", email="alice@example.com", age=30))
            pit.put(Person(name="Bob", email="bob@example.com", age=30))
            pit.put(Person(name="Charlie", email="charlie@example.com", age=25))

            results = pit.search(age=30)
            assert len(results) == 2
            names = {r.name for r in results}
            assert names == {"Alice", "Bob"}

    def test_search_by_string_field(self) -> None:
        """Test searching by string field."""
        with Pit("test", object_types=[Person]) as pit:
            pit.put(Person(name="Alice", email="alice@example.com", age=30))
            pit.put(Person(name="Bob", email="bob@example.com", age=25))

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

    def test_search_by_multiple_fields(self) -> None:
        """Test searching by multiple field values."""
        with Pit("test", object_types=[Person]) as pit:
            pit.put(Person(name="Alice", email="alice@example.com", age=30))
            pit.put(Person(name="Alice", email="alice2@example.com", age=25))
            pit.put(Person(name="Bob", email="bob@example.com", age=30))

            results = pit.search(name="Alice", age=30)
            assert len(results) == 1
            assert results[0].email == "alice@example.com"

    def test_search_no_results(self) -> None:
        """Test search with no matching results."""
        with Pit("test", object_types=[Person]) as pit:
            pit.put(Person(name="Alice", email="alice@example.com", age=30))

            results = pit.search(name="Nobody")
            assert len(results) == 0

    def test_search_empty_criteria(self) -> None:
        """Test search with no criteria returns all."""
        with Pit("test", object_types=[Person]) as pit:
            pit.put(Person(name="A", email="a@t.com", age=1))
            pit.put(Person(name="B", email="b@t.com", age=2))

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

    def test_search_boolean_field(self) -> None:
        """Test searching by boolean field."""
        with Pit("test", object_types=[Product]) as pit:
            pit.put(Product(name="Widget", price=10.0, in_stock=True))
            pit.put(Product(name="Gadget", price=20.0, in_stock=False))

            results = pit.search(in_stock=True)
            assert len(results) == 1
            assert results[0].name == "Widget"

    def test_search_stored(self) -> None:
        """Test search_stored returns StoredObjects with metadata."""
        with Pit("test", object_types=[Person]) as pit:
            pit.put(Person(name="Alice", email="a@t.com", age=30))
            pit.put(Person(name="Bob", email="b@t.com", age=30))

            stored = pit.search_stored(age=30)
            assert len(stored) == 2
            assert all(isinstance(s, StoredObject) for s in stored)
            assert all(s.created_at is not None for s in stored)


class TestPitTagSearch:
    """Tests for tag-based search."""

    def test_search_single_tag(self) -> None:
        """Test searching by single tag."""
        with Pit("test", object_types=[Person]) as pit:
            pit.put(Person(name="A", email="a@t.com", age=1), tags=["important", "urgent"])
            pit.put(Person(name="B", email="b@t.com", age=2), tags=["important"])
            pit.put(Person(name="C", email="c@t.com", age=3), tags=["normal"])

            results = pit.search_tags(["important"])
            assert len(results) == 2

    def test_search_multiple_tags_any(self) -> None:
        """Test searching for any of multiple tags."""
        with Pit("test", object_types=[Person]) as pit:
            pit.put(Person(name="A", email="a@t.com", age=1), tags=["a"])
            pit.put(Person(name="B", email="b@t.com", age=2), tags=["b"])
            pit.put(Person(name="C", email="c@t.com", age=3), tags=["c"])

            results = pit.search_tags(["a", "b"], match_all=False)
            assert len(results) == 2

    def test_search_multiple_tags_all(self) -> None:
        """Test searching for all of multiple tags."""
        with Pit("test", object_types=[Person]) as pit:
            pit.put(Person(name="A", email="a@t.com", age=1), tags=["a", "b", "c"])
            pit.put(Person(name="B", email="b@t.com", age=2), tags=["a", "b"])
            pit.put(Person(name="C", email="c@t.com", age=3), tags=["a"])

            results = pit.search_tags(["a", "b"], match_all=True)
            assert len(results) == 2

    def test_search_empty_tags(self) -> None:
        """Test searching with empty tag list."""
        with Pit("test", object_types=[Person]) as pit:
            pit.put(Person(name="A", email="a@t.com", age=1), tags=["something"])
            results = pit.search_tags([])
            assert len(results) == 0


class TestPitTagManagement:
    """Tests for tag management through Pit."""

    def test_add_tag(self) -> None:
        """Test adding a tag through Pit."""
        with Pit("test", object_types=[Person]) as pit:
            stored = pit.put(Person(name="A", email="a@t.com", age=1))
            result = pit.add_tag(stored.id, "vip")
            assert result is True

            tags = pit.get_tags(stored.id)
            assert tags == ["vip"]

    def test_add_duplicate_tag(self) -> None:
        """Test adding a duplicate tag returns False."""
        with Pit("test", object_types=[Person]) as pit:
            stored = pit.put(Person(name="A", email="a@t.com", age=1), tags=["vip"])
            result = pit.add_tag(stored.id, "vip")
            assert result is False

    def test_remove_tag(self) -> None:
        """Test removing a tag through Pit."""
        with Pit("test", object_types=[Person]) as pit:
            stored = pit.put(Person(name="A", email="a@t.com", age=1), tags=["vip", "active"])
            result = pit.remove_tag(stored.id, "vip")
            assert result is True

            tags = pit.get_tags(stored.id)
            assert tags == ["active"]

    def test_remove_nonexistent_tag(self) -> None:
        """Test removing a tag that doesn't exist."""
        with Pit("test", object_types=[Person]) as pit:
            stored = pit.put(Person(name="A", email="a@t.com", age=1), tags=["vip"])
            result = pit.remove_tag(stored.id, "nonexistent")
            assert result is False

    def test_get_tags_nonexistent_object(self) -> None:
        """Test getting tags for nonexistent object."""
        with Pit("test") as pit:
            tags = pit.get_tags("nonexistent")
            assert tags is None


class TestPitTypeHandling:
    """Tests for handling multiple object types."""

    def test_multiple_types_in_pit(self) -> None:
        """Test storing multiple types in one pit."""
        with Pit("test", object_types=[Person, Product]) as pit:
            person = Person(name="Alice", email="alice@example.com", age=30)
            product = Product(name="Widget", price=10.0)
            stored_person = pit.put(person)
            stored_product = pit.put(product)

            assert pit.count() == 2

            retrieved_person = pit.get(stored_person.id)
            assert isinstance(retrieved_person, Person)
            assert retrieved_person.name == "Alice"

            retrieved_product = pit.get(stored_product.id)
            assert isinstance(retrieved_product, Product)
            assert retrieved_product.name == "Widget"

    def test_search_by_type(self) -> None:
        """Test searching by type_name."""
        with Pit("test", object_types=[Person, Product]) as pit:
            pit.put(Person(name="Alice", email="alice@example.com", age=30))
            pit.put(Person(name="Bob", email="bob@example.com", age=25))
            pit.put(Product(name="Widget", price=10.0))

            people = pit.search(type_name="Person")
            assert len(people) == 2

            products = pit.search(type_name="Product")
            assert len(products) == 1

    def test_register_type(self) -> None:
        """Test registering type after pit creation."""
        with Pit("test") as pit:
            person = Person(name="Alice", email="alice@example.com", age=30)
            stored = pit.put(person)  # Auto-registers Person type

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


class TestPitPersistence:
    """Tests for data persistence."""

    def test_data_persists_to_file(self) -> None:
        """Test that data persists when reopening file-backed pit."""
        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = os.path.join(tmpdir, "persist.duckdb")

            # Create and populate pit
            with Pit("test", db_path=db_path, object_types=[Person]) as pit:
                pit.put(
                    Person(name="Alice", email="alice@example.com", age=30),
                    id="persist-test",
                )

            # Reopen and verify
            with Pit("test", db_path=db_path, object_types=[Person]) as pit:
                retrieved = pit.get("persist-test")
                assert retrieved is not None
                assert retrieved.name == "Alice"


class TestDuckDBConfig:
    """Tests for DuckDB configuration."""

    def test_default_config(self) -> None:
        """Test that default DuckDBConfig is applied."""
        with Pit("test", object_types=[Person]) as pit:
            assert pit.duckdb_config is not None
            assert pit.duckdb_config.memory_limit is None
            assert pit.duckdb_config.threads is None
            assert pit.duckdb_config.preserve_insertion_order is True

    def test_custom_memory_limit(self) -> None:
        """Test setting a custom memory limit."""
        from mosh.pit import DuckDBConfig

        config = DuckDBConfig(memory_limit="256MB")
        with Pit("test", object_types=[Person], duckdb_config=config) as pit:
            assert pit.duckdb_config.memory_limit == "256MB"
            # Verify it works by doing some operations
            pit.put(Person(name="Alice", email="alice@test.com", age=30))
            assert pit.count() == 1

    def test_custom_threads(self) -> None:
        """Test setting a custom thread count."""
        from mosh.pit import DuckDBConfig

        config = DuckDBConfig(threads=2)
        with Pit("test", object_types=[Person], duckdb_config=config) as pit:
            assert pit.duckdb_config.threads == 2
            # Verify it works by doing some operations
            pit.put(Person(name="Bob", email="bob@test.com", age=25))
            assert pit.count() == 1

    def test_temp_directory(self) -> None:
        """Test setting a custom temp directory."""
        from mosh.pit import DuckDBConfig

        with tempfile.TemporaryDirectory() as tmpdir:
            config = DuckDBConfig(temp_directory=tmpdir)
            with Pit("test", object_types=[Person], duckdb_config=config) as pit:
                assert pit.duckdb_config.temp_directory == tmpdir
                # Verify it works
                pit.put(Person(name="Charlie", email="charlie@test.com", age=35))
                assert pit.count() == 1

    def test_preserve_insertion_order_false(self) -> None:
        """Test disabling preserve_insertion_order."""
        from mosh.pit import DuckDBConfig

        config = DuckDBConfig(preserve_insertion_order=False)
        with Pit("test", object_types=[Person], duckdb_config=config) as pit:
            assert pit.duckdb_config.preserve_insertion_order is False
            # Verify it works
            pit.put(Person(name="Diana", email="diana@test.com", age=28))
            assert pit.count() == 1

    def test_full_config(self) -> None:
        """Test setting all DuckDB configuration options."""
        from mosh.pit import DuckDBConfig

        with tempfile.TemporaryDirectory() as tmpdir:
            config = DuckDBConfig(
                memory_limit="512MB",
                threads=4,
                temp_directory=tmpdir,
                preserve_insertion_order=True,
            )
            with Pit("test", object_types=[Person], duckdb_config=config) as pit:
                assert pit.duckdb_config.memory_limit == "512MB"
                assert pit.duckdb_config.threads == 4
                assert pit.duckdb_config.temp_directory == tmpdir
                assert pit.duckdb_config.preserve_insertion_order is True

                # Verify operations work with full config
                for i in range(100):
                    pit.put(
                        Person(name=f"Person{i}", email=f"p{i}@test.com", age=20 + i),
                        id=f"person-{i}",
                    )
                assert pit.count() == 100

    def test_config_with_file_backed_pit(self) -> None:
        """Test DuckDB config works with file-backed pit."""
        from mosh.pit import DuckDBConfig

        with tempfile.TemporaryDirectory() as tmpdir:
            db_path = os.path.join(tmpdir, "config_test.duckdb")
            config = DuckDBConfig(memory_limit="128MB", threads=2)

            with Pit("test", db_path=db_path, object_types=[Person], duckdb_config=config) as pit:
                pit.put(Person(name="Eve", email="eve@test.com", age=22), id="eve")
                assert pit.count() == 1

            # Reopen with same config
            with Pit("test", db_path=db_path, object_types=[Person], duckdb_config=config) as pit:
                retrieved = pit.get("eve")
                assert retrieved is not None
                assert retrieved.name == "Eve"
