from uuid import UUID

from tests.unit_tests.fixtures import get_async_test_client

USER_1_HEADERS = {
    "Authorization": "Bearer user1",
}

USER_2_HEADERS = {
    "Authorization": "Bearer user2",
}

NO_SUCH_USER_HEADERS = {
    "Authorization": "Bearer no_such_user",
}


async def test_health() -> None:
    """Test the health check endpoint."""
    async with get_async_test_client() as client:
        response = await client.get("/health")
        response.raise_for_status()
        assert response.json() == {"status": "ok"}


async def test_create_and_get_collection() -> None:
    """Test creating and retrieving a collection."""
    async with get_async_test_client() as client:
        payload = {"name": "test_collection", "metadata": {"purpose": "unit-test"}}
        response = await client.post(
            "/collections", json=payload, headers=USER_1_HEADERS
        )
        assert response.status_code == 201, (
            f"Failed with error message: {response.text}"
        )
        data = response.json()
        assert data["name"] == "test_collection"
        assert isinstance(UUID(data["uuid"]), UUID)

        # Get collection by ID
        get_response = await client.get(
            f"/collections/{data['uuid']}", headers=USER_1_HEADERS
        )
        assert get_response.status_code == 200
        assert get_response.json()["uuid"] == data["uuid"]

        # Test without metadata
        payload_no_metadata = {"name": "test_collection_no_metadata"}
        response_no_metadata = await client.post(
            "/collections", json=payload_no_metadata, headers=USER_1_HEADERS
        )
        assert response_no_metadata.status_code == 201, (
            f"Failed with error message: {response_no_metadata.text}"
        )
        data_no_metadata = response_no_metadata.json()
        assert data_no_metadata == {
            "uuid": data_no_metadata["uuid"],
            "name": "test_collection_no_metadata",
            "metadata": {
                "owner_id": "user1",
            },
        }


async def test_create_and_list_collection_with_stats() -> None:
    """Test creating and listing a collection with stats."""
    async with get_async_test_client() as client:
        payload = {"name": "test_collection_with_stats", "metadata": {"purpose": "unit-test"}}
        response = await client.post(
            "/collections", json=payload, headers=USER_1_HEADERS
        )
        assert response.status_code == 201, (
            f"Failed with error message: {response.text}"
        )
        data = response.json()
        assert data["name"] == "test_collection_with_stats"
        assert isinstance(UUID(data["uuid"]), UUID)

        # List collections and check for stats
        list_response = await client.get("/collections", headers=USER_1_HEADERS)
        assert list_response.status_code == 200
        collections = list_response.json()
        assert len(collections) > 0
        
        test_collection = next((c for c in collections if c["name"] == "test_collection_with_stats"), None)
        assert test_collection is not None
        assert "document_count" in test_collection
        assert "chunk_count" in test_collection
        assert test_collection["document_count"] == 0
        assert test_collection["chunk_count"] == 0



async def test_create_collections_with_identical_names() -> None:
    """Test that collections with identical names can be created."""
    async with get_async_test_client() as client:
        payload = {"name": "dup_collection", "metadata": {"foo": "bar"}}
        # first create
        r1 = await client.post("/collections", json=payload, headers=USER_1_HEADERS)
        assert r1.status_code == 201

        # second create with same name
        r2 = await client.post("/collections", json=payload, headers=USER_1_HEADERS)
        assert r2.status_code == 201
        assert r1.json()["uuid"] != r2.json()["uuid"]


async def test_create_collection_requires_auth() -> None:
    """POST /collections without a valid token should be 401."""
    async with get_async_test_client() as client:
        payload = {"name": "no_auth", "metadata": {}}
        r = await client.post("/collections", json=payload)
        assert r.status_code == 403
        r2 = await client.post(
            "/collections", json=payload, headers=NO_SUCH_USER_HEADERS
        )
        assert r2.status_code == 401


async def test_get_nonexistent_collection() -> None:
    """GET a collection that doesn't exist should be 404."""
    async with get_async_test_client() as client:
        r = await client.get("/collections/nonexistent", headers=USER_1_HEADERS)
        # Not a UUID, so should be 422
        assert r.status_code == 422

        r = await client.get(
            "/collections/12345678-1234-5678-1234-567812345678", headers=USER_1_HEADERS
        )
        assert r.status_code == 404


async def test_delete_collection_and_nonexistent() -> None:
    """DELETE removes an existing collection and returns 404 on missing."""
    async with get_async_test_client() as client:
        # create first
        payload = {"name": "to_delete", "metadata": {"foo": "bar"}}
        r1 = await client.post("/collections", json=payload, headers=USER_1_HEADERS)
        assert r1.status_code == 201

        # Get the UUID first
        get_collection = await client.get("/collections", headers=USER_1_HEADERS)
        assert get_collection.status_code == 200
        collections = get_collection.json()
        collection_id = next(
            (c["uuid"] for c in collections if c["name"] == "to_delete"), None
        )
        assert collection_id is not None

        # delete it by ID
        r2 = await client.delete(
            f"/collections/{collection_id}", headers=USER_1_HEADERS
        )
        assert r2.status_code == 204

        # Try to get it again by ID
        r3 = await client.get(f"/collections/{collection_id}", headers=USER_1_HEADERS)
        assert r3.status_code == 404

        # Deletion is idempotent
        r4 = await client.delete(
            f"/collections/{collection_id}", headers=USER_1_HEADERS
        )
        assert r4.status_code == 204


async def test_patch_collection() -> None:
    """PATCH should update metadata properly."""
    async with get_async_test_client() as client:
        # create a collection
        payload = {"name": "colA", "metadata": {"a": 1}}
        r = await client.post("/collections", json=payload, headers=USER_1_HEADERS)
        assert r.status_code == 201
        assert r.json() == {
            "uuid": r.json()["uuid"],
            "name": "colA",
            "metadata": {
                "a": 1,
                "owner_id": "user1",
            },
        }

        # Get the UUID for colA
        get_collection = await client.get("/collections", headers=USER_1_HEADERS)
        assert get_collection.status_code == 200
        collections = get_collection.json()
        collection_id = next(
            (c["uuid"] for c in collections if c["name"] == "colA"), None
        )
        assert collection_id is not None

        # update metadata using the UUID
        r2 = await client.patch(
            f"/collections/{collection_id}",
            json={"metadata": {"a": 2}},
            headers=USER_1_HEADERS,
        )
        assert r2.status_code == 200
        assert r2.json() == {
            "uuid": r.json()["uuid"],
            "name": "colA",
            "metadata": {
                "a": 2,
                "owner_id": "user1",
            },
        }


async def test_update_collection_name_and_metadata() -> None:
    """PATCH should rename and/or update metadata properly."""
    async with get_async_test_client() as client:
        # create two collections
        await client.post(
            "/collections",
            json={"name": "colA", "metadata": {"a": 1}},
            headers=USER_1_HEADERS,
        )
        await client.post(
            "/collections",
            json={"name": "colB", "metadata": {"b": 2}},
            headers=USER_1_HEADERS,
        )

        # Get the UUID for colA
        get_collection = await client.get("/collections", headers=USER_1_HEADERS)
        assert get_collection.status_code == 200
        collections = get_collection.json()
        col_a_id = next((c["uuid"] for c in collections if c["name"] == "colA"), None)
        assert col_a_id is not None

        # try renaming colA to colB (conflict)
        no_conflict = await client.patch(
            f"/collections/{col_a_id}",
            json={"name": "colB"},
            headers=USER_1_HEADERS,
        )
        assert no_conflict.status_code == 200
        assert no_conflict.json() == {
            "metadata": {
                "a": 1,
                "owner_id": "user1",
            },
            "name": "colB",
            "uuid": col_a_id,  # The ID should not change
        }

        # rename colA to colC with new metadata (using the UUID we got earlier)
        update = await client.patch(
            f"/collections/{col_a_id}",
            json={"name": "colC", "metadata": {"x": "y"}},
            headers=USER_1_HEADERS,
        )
        assert update.status_code == 200
        body = update.json()
        assert body == {
            "uuid": body["uuid"],
            "name": "colC",
            "metadata": {"x": "y", "owner_id": "user1"},
        }
        # ensure we can get by the ID
        get_by_id = await client.get(f"/collections/{col_a_id}", headers=USER_1_HEADERS)
        assert get_by_id.status_code == 200
        # the ID should remain the same even though name changed
        assert get_by_id.json()["uuid"] == col_a_id
        assert get_by_id.json()["name"] == "colC"

        # update metadata only on colC using the same ID
        meta_update = await client.patch(
            f"/collections/{col_a_id}",
            json={"metadata": {"foo": "bar"}},
            headers=USER_1_HEADERS,
        )
        assert meta_update.status_code == 200
        assert meta_update.json() == {
            "uuid": col_a_id,
            "name": "colC",
            "metadata": {"foo": "bar", "owner_id": "user1"},
        }


async def test_update_nonexistent_collection() -> None:
    """PATCH a missing collection should return 404."""
    async with get_async_test_client() as client:
        r = await client.patch(
            "/collections/does_not_exist",
            json={"metadata": {"any": "thing"}},
            headers=USER_1_HEADERS,
        )
        assert r.status_code == 422

        r = await client.patch(
            "/collections/12345678-1234-5678-1234-567812345678",
            json={"metadata": {"any": "thing"}},
            headers=USER_1_HEADERS,
        )
        assert r.status_code == 404


async def test_list_empty_and_multiple_collections() -> None:
    """Listing when empty and after multiple creates."""
    async with get_async_test_client() as client:
        # ensure database is empty
        empty = await client.get(
            "/collections",
            headers=USER_1_HEADERS,
        )
        assert empty.status_code == 200
        assert empty.json() == []

        # create several
        names = ["one", "two", "three"]
        for n in names:
            r = await client.post(
                "/collections", json={"name": n, "metadata": {}}, headers=USER_1_HEADERS
            )
            assert r.status_code == 201

        listed = await client.get("/collections", headers=USER_1_HEADERS)
        assert listed.status_code == 200
        got = [c["name"] for c in listed.json()]
        for n in names:
            assert n in got


# Check ownership of collections.
async def test_ownership() -> None:
    """Try accessing and deleting collections owned by user 1 using user 2."""
    async with get_async_test_client() as client:
        # create a collection as user 1
        payload = {"name": "owned_by_user1", "metadata": {}}
        r = await client.post("/collections", json=payload, headers=USER_1_HEADERS)
        assert r.status_code == 201

        # Get the UUID of the collection
        get_response = await client.get("/collections", headers=USER_1_HEADERS)
        assert get_response.status_code == 200
        collections = get_response.json()
        collection_id = next(
            (c["uuid"] for c in collections if c["name"] == "owned_by_user1"), None
        )
        assert collection_id is not None

        # user 2 tries to get it by ID
        r2 = await client.get(f"/collections/{collection_id}", headers=USER_2_HEADERS)
        assert r2.status_code == 404

        # Always ack with 204 for idempotency
        r3 = await client.delete(
            f"/collections/{collection_id}", headers=USER_2_HEADERS
        )
        assert r3.status_code == 204

        # Try listing collections as user 2
        r4 = await client.get("/collections", headers=USER_2_HEADERS)
        assert r4.status_code == 200
        assert r4.json() == []

        # Try patching the collection as user 2
        r4 = await client.patch(
            f"/collections/{collection_id}",
            json={"name": "new_name"},
            headers=USER_2_HEADERS,
        )
        assert r4.status_code == 404

        # user 1 can delete it
        r5 = await client.delete(
            f"/collections/{collection_id}", headers=USER_1_HEADERS
        )
        assert r5.status_code == 204
