import os
import functools
import shutil
import tempfile
import pytest
import sqlite3
from typing import Generator, List, Callable, Dict, Union

from chromadb.db.impl.grpc.client import GrpcSysDB
from chromadb.db.impl.grpc.server import GrpcMockSysDB
from chromadb.errors import NotFoundError, UniqueConstraintError, InternalError
from chromadb.test.conftest import find_free_port
from chromadb.types import Collection, Segment, SegmentScope
from chromadb.db.impl.sqlite import SqliteDB
from chromadb.config import (
    DEFAULT_DATABASE,
    DEFAULT_TENANT,
    System,
    Settings,
)
from chromadb.db.system import SysDB
from pytest import FixtureRequest
import uuid
from chromadb.api.configuration import CollectionConfigurationInternal
import logging

logger = logging.getLogger(__name__)

TENANT = "default"
NAMESPACE = "default"

# These are the sample collections that are used in the tests below. Tests can override
# the fields as needed.
sample_collections: List[Collection] = [
    Collection(
        id=uuid.UUID(int=1),
        name="test_collection_1",
        configuration=CollectionConfigurationInternal(),
        metadata={"test_str": "str1", "test_int": 1, "test_float": 1.3},
        dimension=128,
        database=DEFAULT_DATABASE,
        tenant=DEFAULT_TENANT,
        version=0,
    ),
    Collection(
        id=uuid.UUID(int=2),
        name="test_collection_2",
        configuration=CollectionConfigurationInternal(),
        metadata={"test_str": "str2", "test_int": 2, "test_float": 2.3},
        dimension=None,
        database=DEFAULT_DATABASE,
        tenant=DEFAULT_TENANT,
        version=0,
    ),
    Collection(
        id=uuid.UUID(int=3),
        name="test_collection_3",
        configuration=CollectionConfigurationInternal(),
        metadata={"test_str": "str3", "test_int": 3, "test_float": 3.3},
        dimension=None,
        database=DEFAULT_DATABASE,
        tenant=DEFAULT_TENANT,
        version=0,
    ),
    Collection(
        id=uuid.UUID(int=4),
        name="test_collection_4",
        configuration=CollectionConfigurationInternal(),
        metadata={"test_str": "str4", "test_int": 4, "test_float": 4.4},
        dimension=None,
        database=DEFAULT_DATABASE,
        tenant=DEFAULT_TENANT,
        version=0,
    ),
]


def sqlite() -> Generator[SysDB, None, None]:
    """Fixture generator for sqlite DB"""
    db = SqliteDB(
        System(
            Settings(
                allow_reset=True,
            )
        )
    )
    db.start()
    yield db
    db.stop()


def sqlite_persistent() -> Generator[SysDB, None, None]:
    """Fixture generator for sqlite DB"""
    save_path = tempfile.mkdtemp()
    db = SqliteDB(
        System(
            Settings(
                allow_reset=True,
                is_persistent=True,
                persist_directory=save_path,
            )
        )
    )
    db.start()
    yield db
    db.stop()
    if os.path.exists(save_path):
        shutil.rmtree(save_path)


def grpc_with_mock_server() -> Generator[SysDB, None, None]:
    """Fixture generator for sqlite DB that creates a mock grpc sysdb server
    and a grpc client that connects to it."""
    port = find_free_port()

    system = System(
        Settings(
            allow_reset=True,
            chroma_server_grpc_port=port,
        )
    )
    system.instance(GrpcMockSysDB)
    client = system.instance(GrpcSysDB)
    system.start()
    client.reset_and_wait_for_ready()
    yield client
    system.stop()


def grpc_with_real_server() -> Generator[SysDB, None, None]:
    logger.debug("Setting up grpc_with_real_server")
    system = System(
        Settings(
            allow_reset=True,
            chroma_server_grpc_port=50051,
        )
    )
    client = system.instance(GrpcSysDB)
    logger.debug("Starting system")
    system.start()
    logger.debug("Resetting client and waiting for ready")
    client.reset_and_wait_for_ready()
    logger.debug("grpc_with_real_server setup complete")
    yield client
    logger.debug("Stopping system in grpc_with_real_server")
    system.stop()


def db_fixtures() -> List[Callable[[], Generator[SysDB, None, None]]]:
    if "CHROMA_CLUSTER_TEST_ONLY" in os.environ:
        return [grpc_with_real_server]
    else:
        return [sqlite, sqlite_persistent, grpc_with_mock_server]


@pytest.fixture(scope="module", params=db_fixtures())
def sysdb(request: FixtureRequest) -> Generator[SysDB, None, None]:
    logger.debug(f"Setting up sysdb fixture with {request.param.__name__}")
    yield next(request.param())
    logger.debug("Tearing down sysdb fixture")

def sample_segment(collection_id: uuid.UUID = uuid.uuid4(),
                   segment_type: str = "test_type_a",
                   scope: SegmentScope = SegmentScope.VECTOR,
                   metadata: Dict[str, Union[str, int, float]] = {
                       "test_str": "str1",
                       "test_int": 1,
                       "test_float": 1.3,
                   },
) -> Segment:
    return Segment(
        id=uuid.uuid4(),
        type=segment_type,
        scope=scope,
        collection=collection_id,
        metadata=metadata,
        file_paths={},
    )

# region Collection tests
def test_create_get_delete_collections(sysdb: SysDB) -> None:
    logger.debug("Resetting state")
    sysdb.reset_state()

    segments_created_with_collection = []
    for collection in sample_collections:
        logger.debug(f"Creating collection: {collection.name}")
        segments = [
            sample_segment(collection_id=collection.id, scope=SegmentScope.METADATA),
            sample_segment(collection_id=collection.id, scope=SegmentScope.RECORD),
            sample_segment(collection_id=collection.id, scope=SegmentScope.VECTOR),
        ]
        segments_created_with_collection.extend(segments)
        sysdb.create_collection(
            id=collection.id,
            name=collection.name,
            configuration=collection.get_configuration(),
            segments=segments,
            metadata=collection["metadata"],
            dimension=collection["dimension"],
        )
        collection["database"] = DEFAULT_DATABASE
        collection["tenant"] = DEFAULT_TENANT

    logger.debug("Getting all collections")
    results = sysdb.get_collections()
    results = sorted(results, key=lambda c: c.name)

    assert sorted(results, key=lambda c: c.name) == sample_collections

    # Duplicate create fails
    with pytest.raises(UniqueConstraintError):
        sysdb.create_collection(
            name=sample_collections[0].name,
            id=sample_collections[0].id,
            configuration=sample_collections[0].get_configuration(),
            segments=[segments_created_with_collection[0]],
        )

    # Find by name
    for collection in sample_collections:
        result = sysdb.get_collections(name=collection["name"])
        assert result == [collection]

    # Find by id
    for collection in sample_collections:
        result = sysdb.get_collections(id=collection["id"])
        assert result == [collection]

    # Verify segment information
    for collection in sample_collections:
        collection_with_segments_result = sysdb.get_collection_with_segments(collection.id)
        assert collection_with_segments_result["collection"] == collection
        assert all([segment["collection"] == collection.id for segment in collection_with_segments_result["segments"]])
        
    # Delete
    c1 = sample_collections[0]
    sysdb.delete_collection(id=c1.id)

    results = sysdb.get_collections()
    assert c1 not in results
    assert len(results) == len(sample_collections) - 1
    assert sorted(results, key=lambda c: c.name) == sample_collections[1:]

    by_id_result = sysdb.get_collections(id=c1["id"])
    assert by_id_result == []

    # Check that the segment was deleted
    by_collection_result = sysdb.get_segments(collection=c1.id)
    assert by_collection_result == []

    # Duplicate delete throws an exception
    with pytest.raises(NotFoundError):
        sysdb.delete_collection(c1.id)


    # Create a new collection with two segments that have same id.
    # Creation should fail.
    # Check that collection was not created.
    # Check that segments were not created.
    with pytest.raises((InternalError, UniqueConstraintError, sqlite3.IntegrityError)):
        sysdb.create_collection(
            name=sample_collections[0].name,
            id=sample_collections[0].id,
            configuration=sample_collections[0].get_configuration(),
            segments=[segments_created_with_collection[0], segments_created_with_collection[0]],
        )
    # Check that collection was not created.
    # Check that segments were not created.
    by_id_result = sysdb.get_collections(id=sample_collections[0].id)
    assert by_id_result == []
    by_collection_result = sysdb.get_segments(collection=sample_collections[0].id)
    assert by_collection_result == []

def test_count_collections(sysdb: SysDB) -> None:
    logger.debug("Resetting state")
    sysdb.reset_state()

    segments_created_with_collection = []
    for collection in sample_collections:
        logger.debug(f"Creating collection: {collection.name}")
        segments = [
            sample_segment(collection_id=collection.id, scope=SegmentScope.METADATA),
            sample_segment(collection_id=collection.id, scope=SegmentScope.RECORD),
            sample_segment(collection_id=collection.id, scope=SegmentScope.VECTOR),
        ]
        segments_created_with_collection.extend(segments)
        sysdb.create_collection(
            id=collection.id,
            name=collection.name,
            configuration=collection.get_configuration(),
            segments=segments,
            metadata=collection["metadata"],
            dimension=collection["dimension"],
            tenant = DEFAULT_TENANT,
            database = DEFAULT_DATABASE,
        )
        collection["database"] = DEFAULT_DATABASE
        collection["tenant"] = DEFAULT_TENANT

    collection_count = sysdb.count_collections(tenant=DEFAULT_TENANT, database=DEFAULT_DATABASE)
    logger.debug(f"Collection count: {collection_count}")

    assert collection_count == len(sample_collections)

    # without specifying the database name should give count of all collections for the tenant
    collection_count = sysdb.count_collections(tenant=DEFAULT_TENANT)
    logger.debug(f"Collection count: {collection_count}")

    assert collection_count == len(sample_collections)

def test_get_collection_size(sysdb: SysDB) -> None:
    if not isinstance(sysdb, GrpcSysDB):
        pytest.skip("Skipping because this functionality is only supported by GrpcSysDB")
    
    sysdb.reset_state()

    for collection in sample_collections:
        collection_size = sysdb.get_collection_size(collection.id)
        assert collection_size == 0

def test_update_collections(sysdb: SysDB) -> None:
    coll = Collection(
        name=sample_collections[0].name,
        id=sample_collections[0].id,
        configuration=sample_collections[0].get_configuration(),
        metadata=sample_collections[0]["metadata"],
        dimension=sample_collections[0]["dimension"],
        database=DEFAULT_DATABASE,
        tenant=DEFAULT_TENANT,
        version=0,
    )

    sysdb.reset_state()

    sysdb.create_collection(
        id=coll.id,
        name=coll.name,
        configuration=coll.get_configuration(),
        segments=[
            Segment(
                id=uuid.uuid4(),
                type="test_type_a",
                scope=SegmentScope.VECTOR,
                collection=coll.id,
                metadata={"test_str": "str1", "test_int": 1, "test_float": 1.3},
                file_paths={},
            )
        ],
        metadata=coll["metadata"],
        dimension=coll["dimension"],
    )

    # Update name
    coll["name"] = "new_name"
    sysdb.update_collection(coll.id, name=coll.name)
    result = sysdb.get_collections(name=coll.name)
    assert result == [coll]

    # Update dimension
    coll["dimension"] = 128
    sysdb.update_collection(coll.id, dimension=coll.dimension)
    result = sysdb.get_collections(id=coll["id"])
    assert result == [coll]

    # Reset the metadata
    coll["metadata"] = {"test_str2": "str2"}
    sysdb.update_collection(coll.id, metadata=coll["metadata"])
    result = sysdb.get_collections(id=coll["id"])
    assert result == [coll]

    # Delete all metadata keys
    coll["metadata"] = None
    sysdb.update_collection(coll.id, metadata=None)
    result = sysdb.get_collections(id=coll["id"])
    assert result == [coll]


def test_get_or_create_collection(sysdb: SysDB) -> None:
    sysdb.reset_state()

    # get_or_create = True returns existing collection
    collection = sample_collections[0]

    sysdb.create_collection(
        id=collection.id,
        name=collection.name,
        configuration=collection.get_configuration(),
        segments=[
            Segment(
                id=uuid.uuid4(),
                type="test_type_a",
                scope=SegmentScope.VECTOR,
                collection=collection.id,
                metadata={"test_str": "str1", "test_int": 1, "test_float": 1.3},
                file_paths={},
            )
        ],
        metadata=collection["metadata"],
        dimension=collection["dimension"],
    )

    # Create collection with same name, but different id.
    # Since get_or_create is true, it should return the existing collection.
    result, created = sysdb.create_collection(
        name=collection.name,
        id=uuid.uuid4(),
        configuration=collection.get_configuration(),
        get_or_create=True,
        segments=[
            Segment(
                id=uuid.uuid4(),
                type="test_type_a",
                scope=SegmentScope.VECTOR,
                collection=sample_collections[1].id,
                metadata={"test_str": "str1", "test_int": 1, "test_float": 1.3},
                file_paths={},
            )
        ], # This could have been empty - [].
        metadata=collection["metadata"],
    )
    assert result == collection

    # Only one collection with the same name exists
    get_result = sysdb.get_collections(name=collection["name"])
    assert get_result == [collection]

    # get_or_create = True creates new collection
    result, created = sysdb.create_collection(
        name=sample_collections[1].name,
        id=sample_collections[1].id,
        configuration=sample_collections[1].get_configuration(),
        segments=[
            Segment(
                id=uuid.uuid4(),
                type="test_type_a",
                scope=SegmentScope.VECTOR,
                collection=sample_collections[1].id,
                metadata={"test_str": "str1", "test_int": 1, "test_float": 1.3},
                file_paths={},
            )
        ],
        get_or_create=True,
        metadata=sample_collections[1]["metadata"],
    )
    assert result == sample_collections[1]

    # get_or_create = False creates new collection
    result, created = sysdb.create_collection(
        name=sample_collections[2].name,
        id=sample_collections[2].id,
        configuration=sample_collections[2].get_configuration(),
        segments=[
            Segment(
                id=uuid.uuid4(),
                type="test_type_a",
                scope=SegmentScope.VECTOR,
                collection=sample_collections[2].id,
                metadata={"test_str": "str1", "test_int": 1, "test_float": 1.3},
                file_paths={},
            )
        ],
        get_or_create=False,
        metadata=sample_collections[2]["metadata"],
    )
    assert result == sample_collections[2]

    # get_or_create = False fails if collection already exists
    with pytest.raises(UniqueConstraintError):
        sysdb.create_collection(
            name=sample_collections[2].name,
            id=sample_collections[2].id,
            configuration=sample_collections[2].get_configuration(),
            get_or_create=False,
            segments=[
                Segment(
                    id=uuid.uuid4(),
                    type="test_type_a",
                    scope=SegmentScope.VECTOR,
                    collection=sample_collections[2].id,
                    metadata={"test_str": "str1", "test_int": 1, "test_float": 1.3},
                    file_paths={},
                )
            ],
            metadata=collection["metadata"],
        )

    # get_or_create = True does not overwrite metadata
    overlayed_metadata: Dict[str, Union[str, int, float]] = {
        "test_new_str": "new_str",
        "test_int": 1,
    }

    result, created = sysdb.create_collection(
        name=sample_collections[2].name,
        id=sample_collections[2].id,
        configuration=sample_collections[2].get_configuration(),
        segments=[
            Segment(
                id=uuid.uuid4(),
                type="test_type_a",
                scope=SegmentScope.VECTOR,
                collection=sample_collections[2].id,
                metadata={"test_str": "str1", "test_int": 1, "test_float": 1.3},
                file_paths={},
            )
        ],
        get_or_create=True,
        metadata=overlayed_metadata,
    )

    assert result["metadata"] != overlayed_metadata
    assert result["metadata"] == sample_collections[2]["metadata"]

    # get_or_create = True with None metadata does not overwrite metadata
    result, created = sysdb.create_collection(
        name=sample_collections[2].name,
        id=sample_collections[2].id,
        configuration=sample_collections[2].get_configuration(),
        segments=[sample_segment(sample_collections[2].id)],
        get_or_create=True,
        metadata=None,
    )
    assert result["metadata"] == sample_collections[2]["metadata"]


def test_create_get_delete_database_and_collection(sysdb: SysDB) -> None:
    sysdb.reset_state()
    # Check that collection doesn't exist in new_database
    result = sysdb.get_collections(id=sample_collections[1].id, database="new_database")
    assert len(result) == 0

    # Create a new database
    sysdb.create_database(id=uuid.uuid4(), name="new_database")

    # Create a new collection in the new database
    sysdb.create_collection(
        id=sample_collections[0].id,
        name=sample_collections[0].name,
        configuration=sample_collections[0].get_configuration(),
        segments=[sample_segment(sample_collections[0].id)],
        metadata=sample_collections[0]["metadata"],
        dimension=sample_collections[0]["dimension"],
        database="new_database",
    )

    # Create a new collection with the same id but different name in the new database
    # and expect an error
    with pytest.raises(UniqueConstraintError):
        sysdb.create_collection(
            id=sample_collections[0].id,
            name="new_name",
            configuration=sample_collections[0].get_configuration(),
            metadata=sample_collections[0]["metadata"],
            dimension=sample_collections[0]["dimension"],
            segments=[sample_segment(sample_collections[0].id)],
            database="new_database",
            get_or_create=False,
        )

    # Create a new collection in the default database
    sysdb.create_collection(
        id=sample_collections[1].id,
        name=sample_collections[1].name,
        configuration=sample_collections[1].get_configuration(),
        metadata=sample_collections[1]["metadata"],
        dimension=sample_collections[1]["dimension"],
        segments=[sample_segment(sample_collections[1].id)],
    )

    # Check that the new database and collections exist
    result = sysdb.get_collections(
        name=sample_collections[0]["name"], database="new_database"
    )
    assert len(result) == 1
    sample_collections[0]["database"] = "new_database"
    assert result[0] == sample_collections[0]

    # Check that the collection in the default database exists
    result = sysdb.get_collections(name=sample_collections[1]["name"])
    assert len(result) == 1
    assert result[0] == sample_collections[1]

    # Get for a database that doesn't exist with a name that exists in the new database and expect no results
    assert (
        len(
            sysdb.get_collections(
                name=sample_collections[0]["name"], database="fake_db"
            )
        )
        == 0
    )

    # Delete the collection in the new database
    sysdb.delete_collection(id=sample_collections[0].id, database="new_database")

    # Check that the collection in the new database was deleted
    result = sysdb.get_collections(database="new_database")
    assert len(result) == 0

    # Check that the collection in the default database still exists
    result = sysdb.get_collections(name=sample_collections[1].name)
    assert len(result) == 1
    assert result[0] == sample_collections[1]

    # Delete the deleted collection in the default database and expect an error
    with pytest.raises(NotFoundError):
        sysdb.delete_collection(id=sample_collections[0].id)

    # Delete the existing collection in the new database and expect an error
    with pytest.raises(NotFoundError):
        sysdb.delete_collection(id=sample_collections[1].id, database="new_database")

def test_create_update_with_database(sysdb: SysDB) -> None:
    sysdb.reset_state()

    # Create a new database
    sysdb.create_database(id=uuid.uuid4(), name="new_database")

    # Create a new collection in the new database
    sysdb.create_collection(
        id=sample_collections[0].id,
        name=sample_collections[0].name,
        configuration=sample_collections[0].get_configuration(),
        segments=[sample_segment(sample_collections[0].id)],
        metadata=sample_collections[0]["metadata"],
        dimension=sample_collections[0]["dimension"],
        database="new_database",
    )

    # Create a new collection in the default database
    sysdb.create_collection(
        id=sample_collections[1].id,
        name=sample_collections[1].name,
        configuration=sample_collections[1].get_configuration(),
        segments=[sample_segment(sample_collections[1].id)],
        metadata=sample_collections[1]["metadata"],
        dimension=sample_collections[1]["dimension"],
    )

    # Update the collection in the default database
    sysdb.update_collection(
        id=sample_collections[1].id,
        name="new_name_1",
    )

    # Check that the collection in the default database was updated
    result = sysdb.get_collections(id=sample_collections[1]["id"])
    assert len(result) == 1
    assert result[0]["name"] == "new_name_1"

    # Update the collection in the new database
    sysdb.update_collection(
        id=sample_collections[0].id,
        name="new_name_0",
    )

    # Check that the collection in the new database was updated
    result = sysdb.get_collections(
        id=sample_collections[0]["id"], database="new_database"
    )
    assert len(result) == 1
    assert result[0]["name"] == "new_name_0"

    # Try to create the collection in the default database in the new database and expect an error
    with pytest.raises(UniqueConstraintError):
        sysdb.create_collection(
            id=sample_collections[1].id,
            name=sample_collections[1].name,
            configuration=sample_collections[1].get_configuration(),
            segments=[sample_segment(sample_collections[1].id)],
            metadata=sample_collections[1]["metadata"],
            dimension=sample_collections[1]["dimension"],
            database="new_database",
        )


def test_get_multiple_with_database(sysdb: SysDB) -> None:
    sysdb.reset_state()

    # Create a new database
    sysdb.create_database(id=uuid.uuid4(), name="new_database")

    # Create sample collections in the new database
    for collection in sample_collections:
        sysdb.create_collection(
            id=collection.id,
            name=collection.name,
            configuration=collection.get_configuration(),
            segments=[sample_segment(collection.id)],
            metadata=collection["metadata"],
            dimension=collection["dimension"],
            database="new_database",
        )
        collection["database"] = "new_database"

    # Get all collections in the new database
    result = sysdb.get_collections(database="new_database")
    assert len(result) == len(sample_collections)
    assert sorted(result, key=lambda c: c.name) == sample_collections

    # Get all collections in the default database
    result = sysdb.get_collections()
    assert len(result) == 0

def test_create_database_with_tenants(sysdb: SysDB) -> None:
    sysdb.reset_state()

    # Create a new tenant
    sysdb.create_tenant(name="tenant1")

    # Create tenant that already exits and expect an error
    with pytest.raises(UniqueConstraintError):
        sysdb.create_tenant(name="tenant1")

    with pytest.raises(UniqueConstraintError):
        sysdb.create_tenant(name=DEFAULT_TENANT)

    # Create a new database within this tenant and also in the default tenant
    sysdb.create_database(id=uuid.uuid4(), name="new_database", tenant="tenant1")
    sysdb.create_database(id=uuid.uuid4(), name="new_database")

    # Create a new collection in the new tenant
    sysdb.create_collection(
        id=sample_collections[0].id,
        name=sample_collections[0].name,
        configuration=sample_collections[0].get_configuration(),
        segments=[sample_segment(sample_collections[0].id)],
        metadata=sample_collections[0]["metadata"],
        dimension=sample_collections[0]["dimension"],
        database="new_database",
        tenant="tenant1",
    )
    sample_collections[0]["tenant"] = "tenant1"
    sample_collections[0]["database"] = "new_database"

    # Create a new collection in the default tenant
    sysdb.create_collection(
        id=sample_collections[1].id,
        name=sample_collections[1].name,
        configuration=sample_collections[1].get_configuration(),
        segments=[sample_segment(sample_collections[1].id)],
        metadata=sample_collections[1]["metadata"],
        dimension=sample_collections[1]["dimension"],
        database="new_database",
    )

    sample_collections[1]["database"] = "new_database"

    # Check that both tenants have the correct collections
    result = sysdb.get_collections(database="new_database", tenant="tenant1")
    assert len(result) == 1
    assert result[0] == sample_collections[0]

    result = sysdb.get_collections(database="new_database")
    assert len(result) == 1
    assert result[0] == sample_collections[1]

    # Creating a collection id that already exists in a tenant that does not have it
    # should error
    with pytest.raises(UniqueConstraintError):
        sysdb.create_collection(
            id=sample_collections[0].id,
            name=sample_collections[0].name,
            configuration=sample_collections[0].get_configuration(),
            segments=[sample_segment(sample_collections[0].id)],
            metadata=sample_collections[0]["metadata"],
            dimension=sample_collections[0]["dimension"],
            database="new_database",
        )

    with pytest.raises(UniqueConstraintError):
        sysdb.create_collection(
            id=sample_collections[1].id,
            name=sample_collections[1].name,
            configuration=sample_collections[1].get_configuration(),
            segments=[sample_segment(sample_collections[1].id)],
            metadata=sample_collections[1]["metadata"],
            dimension=sample_collections[1]["dimension"],
            database="new_database",
            tenant="tenant1",
        )

    # A new tenant DOES NOT have a default database. This does not error, instead 0
    # results are returned
    result = sysdb.get_collections(database=DEFAULT_DATABASE, tenant="tenant1")
    assert len(result) == 0


def test_get_database_with_tenants(sysdb: SysDB) -> None:
    sysdb.reset_state()

    # Create a new tenant
    sysdb.create_tenant(name="tenant1")

    # Get the tenant and check that it exists
    result = sysdb.get_tenant(name="tenant1")
    assert result["name"] == "tenant1"

    # Get a tenant that does not exist and expect an error
    with pytest.raises(NotFoundError):
        sysdb.get_tenant(name="tenant2")

    # Create a new database within this tenant
    sysdb.create_database(id=uuid.uuid4(), name="new_database", tenant="tenant1")

    # Get the database and check that it exists
    result = sysdb.get_database(name="new_database", tenant="tenant1")
    assert result["name"] == "new_database"
    assert result["tenant"] == "tenant1"

    # Get a database that does not exist in a tenant that does exist and expect an error
    with pytest.raises(NotFoundError):
        sysdb.get_database(name="new_database1", tenant="tenant1")

    # Get a database that does not exist in a tenant that does not exist and expect an
    # error
    with pytest.raises(NotFoundError):
        sysdb.get_database(name="new_database1", tenant="tenant2")


# endregion

# region Segment tests
sample_segments = [
    Segment(
        id=uuid.UUID("00000000-d7d7-413b-92e1-731098a6e492"),
        type="test_type_a",
        scope=SegmentScope.VECTOR,
        collection=sample_collections[0].id,
        metadata={"test_str": "str1", "test_int": 1, "test_float": 1.3},
        file_paths={},
    ),
    Segment(
        id=uuid.UUID("11111111-d7d7-413b-92e1-731098a6e492"),
        type="test_type_b",
        scope=SegmentScope.VECTOR,
        collection=sample_collections[1].id,
        metadata={"test_str": "str2", "test_int": 2, "test_float": 2.3},
        file_paths={},
    ),
]


def test_create_get_delete_segments(sysdb: SysDB) -> None:
    sysdb.reset_state()

    # Keep track of segments created with a collection.
    segments_created_with_collection = []
    # Used to toggle between test_type_a and test_type_b
    toggle_type = False

    # Create collections along with segments.
    for collection in sample_collections:
        toggle_type = not toggle_type
        segment = sample_segment(
            collection_id=collection.id,
            segment_type="test_type_a" if toggle_type else "test_type_b",
        )
        segments_created_with_collection.append(segment)
        collection_result, created = sysdb.create_collection(
            id=collection.id,
            name=collection.name,
            configuration=collection.get_configuration(),
            segments=[segment],
            metadata=collection["metadata"],
            dimension=collection["dimension"],
        )
        assert created is True

    results: List[Segment] = []
    for collection in sample_collections:
        results.extend(sysdb.get_segments(collection=collection.id))
    results = sorted(results, key=lambda c: c["id"])
    sorted_segments = sorted(segments_created_with_collection, key=lambda c: c["id"])
    assert results == sorted_segments

    # Duplicate create fails
    with pytest.raises(UniqueConstraintError):
        sysdb.create_segment(segments_created_with_collection[0])

    # Find by id
    for segment in segments_created_with_collection:
        result = sysdb.get_segments(id=segment["id"], collection=segment["collection"])
        assert result == [segment]

    # Find by type
    result = sysdb.get_segments(type="test_type_a", collection=sample_collections[0].id)
    assert len(result) == 1
    assert result[0]["collection"] == sample_collections[0].id
    assert result[0] == segments_created_with_collection[0]

    result = sysdb.get_segments(type="test_type_b", collection=sample_collections[1].id)
    assert len(result) == 1
    assert result[0] == segments_created_with_collection[1]

    # Find by collection ID
    result = sysdb.get_segments(collection=sample_collections[0].id)
    assert len(result) == 1
    assert result[0] == segments_created_with_collection[0]

    # Find by type and collection ID (positive case)
    result = sysdb.get_segments(type="test_type_a", collection=sample_collections[0].id)
    assert len(result) == 1
    assert result[0] == segments_created_with_collection[0]

    # Find by type and collection ID (negative case)
    result = sysdb.get_segments(type="test_type_b", collection=sample_collections[0].id)
    assert len(result) == 0

    # Delete Segments will be a NoOp due to atomic delete of collection and segments.
    # See comments in coordinator.go for more details.
    # Execute the call and expect no error or exception.
    s1 = segments_created_with_collection[0]
    sysdb.delete_segment(s1["collection"], s1["id"])

def test_update_segment(sysdb: SysDB) -> None:
    metadata: Dict[str, Union[str, int, float]] = {
        "test_str": "str1",
        "test_int": 1,
        "test_float": 1.3,
    }
    segment = Segment(
        id=uuid.uuid4(),
        type="test_type_a",
        scope=SegmentScope.VECTOR,
        collection=sample_collections[0].id,
        metadata=metadata,
        file_paths={},
    )

    sysdb.reset_state()
    for c in sample_collections:
        sysdb.create_collection(
            id=c.id,
            name=c.name,
            configuration=c.get_configuration(),
            segments=[sample_segment(c.id)],
            metadata=c["metadata"],
            dimension=c["dimension"],
        )

    sysdb.create_segment(segment)

    # TODO: revisit update segment - push collection id

    result = sysdb.get_segments(id=segment["id"], collection=segment["collection"])
    result[0]["collection"] = segment["collection"]
    assert result == [segment]

    result = sysdb.get_segments(id=segment["id"], collection=segment["collection"])
    result[0]["collection"] = segment["collection"]
    assert result == [segment]

    # Add a new metadata key
    metadata["test_str2"] = "str2"
    sysdb.update_segment(
        segment["collection"], segment["id"], metadata={"test_str2": "str2"}
    )
    result = sysdb.get_segments(id=segment["id"], collection=segment["collection"])
    result[0]["collection"] = segment["collection"]
    assert result == [segment]

    # Update a metadata key
    metadata["test_str"] = "str3"
    sysdb.update_segment(
        segment["collection"], segment["id"], metadata={"test_str": "str3"}
    )
    result = sysdb.get_segments(id=segment["id"], collection=segment["collection"])
    result[0]["collection"] = segment["collection"]
    assert result == [segment]

    # Delete a metadata key
    del metadata["test_str"]
    sysdb.update_segment(
        segment["collection"], segment["id"], metadata={"test_str": None}
    )
    result = sysdb.get_segments(id=segment["id"], collection=segment["collection"])
    result[0]["collection"] = segment["collection"]
    assert result == [segment]

    # Delete all metadata keys
    segment["metadata"] = None
    sysdb.update_segment(segment["collection"], segment["id"], metadata=None)
    result = sysdb.get_segments(id=segment["id"], collection=segment["collection"])
    result[0]["collection"] = segment["collection"]
    assert result == [segment]


# endregion
