import numbers

import numpy as np
import pytest
from base.client_v2_base import TestMilvusClientV2Base
from common import common_func as cf
from common import common_type as ct
from common.common_type import CaseLabel, CheckTasks
from utils.util_log import test_log as log
from utils.util_pymilvus import DataType

prefix = "alter"
default_vector_field_name = "vector"
default_primary_key_field_name = "id"
default_string_field_name = "varchar"
default_float_field_name = "float"
default_new_field_name = "field_new"
default_dynamic_field_name = "dynamic_field"
exp_res = "exp_res"
default_nb = 20
default_dim = 128
default_limit = 10
default_warmup_dim = 128
default_warmup_nb = ct.default_nb


class TestMilvusClientAlterIndex(TestMilvusClientV2Base):
    @pytest.mark.tags(CaseLabel.L0)
    def test_milvus_client_alter_index_default(self):
        """
        target: test alter index
        method: 1. alter index after load
                verify alter fail
                2. alter index after release
                verify alter successfully
                3. drop index properties after load
                verify drop fail
                4. drop index properties after release
                verify drop successfully
        expected: alter successfully
        """
        client = self._client()
        collection_name = cf.gen_collection_name_by_testcase_name()
        self.create_collection(client, collection_name, ct.default_dim, consistency_level="Strong")
        idx_names, _ = self.list_indexes(client, collection_name, field_name=default_vector_field_name)
        self.load_collection(client, collection_name)
        res1 = self.describe_index(client, collection_name, index_name=idx_names[0])[0]
        assert res1.get("mmap.enabled", None) is None
        error = {
            ct.err_code: 104,
            ct.err_msg: f"can't alter index on loaded collection, "
            f"please release the collection first: collection already loaded[collection={collection_name}]",
        }
        # 1. alter index after load
        self.alter_index_properties(
            client,
            collection_name,
            idx_names[0],
            properties={"mmap.enabled": True},
            check_task=CheckTasks.err_res,
            check_items=error,
        )
        self.drop_index_properties(
            client,
            collection_name,
            idx_names[0],
            property_keys=["mmap.enabled"],
            check_task=CheckTasks.err_res,
            check_items=error,
        )
        self.release_collection(client, collection_name)
        # 2. alter index after release
        self.alter_index_properties(client, collection_name, idx_names[0], properties={"mmap.enabled": True})
        res2 = self.describe_index(client, collection_name, index_name=idx_names[0])[0]
        assert res2.get("mmap.enabled", None) == "True"
        self.drop_index_properties(client, collection_name, idx_names[0], property_keys=["mmap.enabled"])
        res3 = self.describe_index(client, collection_name, index_name=idx_names[0])[0]
        assert res3.get("mmap.enabled", None) is None

    @pytest.mark.tags(CaseLabel.L1)
    def test_milvus_client_alter_index_unsupported_properties(self):
        """
        target: test alter index with unsupported properties
        method: 1. alter index with unsupported properties
        expected: raise exception
        """
        client = self._client()
        collection_name = cf.gen_collection_name_by_testcase_name()
        # 1. create collection
        schema = self.create_schema(client, enable_dynamic_field=False)[0]
        dim = 32
        pk_field_name = "id_string"
        vector_field_name = "embeddings"
        str_field_name = "title"
        max_length = 16
        schema.add_field(pk_field_name, DataType.VARCHAR, max_length=max_length, is_primary=True, auto_id=False)
        schema.add_field(vector_field_name, DataType.FLOAT_VECTOR, dim=dim, mmap_enabled=True)
        schema.add_field(str_field_name, DataType.VARCHAR, max_length=max_length, mmap_enabled=True)

        index_params = self.prepare_index_params(client)[0]
        index_params.add_index(
            field_name=vector_field_name,
            metric_type="COSINE",
            index_type="HNSW",
            params={"M": 16, "efConstruction": 100, "mmap.enabled": True},
        )
        index_params.add_index(field_name=str_field_name)
        self.create_collection(
            client, collection_name, schema=schema, index_params=index_params, properties={"mmap.enabled": True}
        )
        self.describe_collection(
            client,
            collection_name,
            check_task=CheckTasks.check_collection_fields_properties,
            check_items={
                str_field_name: {"max_length": max_length, "mmap_enabled": True},
                vector_field_name: {"mmap_enabled": True},
                "properties": {"mmap.enabled": "False"},
            },
        )
        res = self.describe_index(client, collection_name, index_name=vector_field_name)[0]
        assert res.get("mmap.enabled", None) == "True"
        self.release_collection(client, collection_name)
        properties = self.describe_index(client, collection_name, index_name=vector_field_name)[0]
        for p in properties.items():
            if p[0] not in ["mmap.enabled"]:
                log.debug(f"try to alter index property: {p[0]}")
                error = {ct.err_code: 1, ct.err_msg: f"{p[0]} is not a configable index property"}
                new_value = p[1] + 1 if isinstance(p[1], numbers.Number) else "new_value"
                self.alter_index_properties(
                    client,
                    collection_name,
                    vector_field_name,
                    properties={p[0]: new_value},
                    check_task=CheckTasks.err_res,
                    check_items=error,
                )

    @pytest.mark.tags(CaseLabel.L1)
    def test_milvus_client_alter_index_unsupported_value(self):
        """
        target: test alter index with unsupported properties
        method: 1. alter index with unsupported properties
        expected: raise exception
        """
        client = self._client()
        collection_name = cf.gen_collection_name_by_testcase_name()
        self.create_collection(client, collection_name, ct.default_dim, consistency_level="Strong")
        idx_names, _ = self.list_indexes(client, collection_name, field_name=default_vector_field_name)
        self.release_collection(client, collection_name)
        res1 = self.describe_index(client, collection_name, index_name=idx_names[0])[0]
        assert res1.get("mmap.enabled", None) is None
        unsupported_values = [None, [], "", 20, "  ", 0.01, "new_value"]
        for value in unsupported_values:
            error = {ct.err_code: 1, ct.err_msg: "invalid mmap.enabled value"}
            self.alter_index_properties(client, collection_name, idx_names[0],
                                        properties={"mmap.enabled": value},
                                        check_task=CheckTasks.err_res, check_items=error)

    @pytest.mark.tags(CaseLabel.L1)
    def test_milvus_client_create_index_idempotent_with_field_warmup(self):
        """
        target: test create index idempotency when warmup is set at field level
        method: 1. create collection
                2. release collection
                3. alter field to set warmup=sync at field level (this adds warmup to field TypeParams)
                4. drop existing index
                5. create index (first time)
                6. create same index again (second time - should be idempotent)
        expected: both create_index calls should succeed (idempotent behavior)
        issue: https://github.com/milvus-io/milvus/issues/XXXXX
        note: This test verifies the fix for checkParams function in index_meta.go
              which was missing WarmupKey in DeleteParams, causing idempotency check to fail.
              When index is created, WarmupKey is removed from stored TypeParams.
              When checking idempotency, WarmupKey should also be filtered from request TypeParams.
        """
        client = self._client()
        collection_name = cf.gen_collection_name_by_testcase_name()
        # 1. create collection
        self.create_collection(client, collection_name, default_dim, consistency_level="Strong")
        # 2. release collection before altering field properties
        self.release_collection(client, collection_name)
        # 3. alter field to set warmup=sync at field level
        # This adds warmup to field TypeParams, which will be included in CreateIndex request
        self.alter_collection_field(
            client, collection_name, field_name=default_vector_field_name, field_params={"warmup": "sync"}
        )
        # 4. drop existing index
        self.drop_index(client, collection_name, default_vector_field_name)
        res = self.list_indexes(client, collection_name)[0]
        assert res == []
        # 5. prepare index params
        index_params = self.prepare_index_params(client)[0]
        index_params.add_index(
            field_name=default_vector_field_name,
            index_type="HNSW",
            metric_type="L2",
            params={"M": 8, "efConstruction": 200},
        )
        # 6. create index (first time) - should succeed
        self.create_index(client, collection_name, index_params)
        idx_names, _ = self.list_indexes(client, collection_name, field_name=default_vector_field_name)
        assert len(idx_names) == 1
        # 7. create same index again (second time) - should be idempotent and succeed
        # Before fix: this would fail with "at most one distinct index is allowed per field"
        # because checkParams didn't filter WarmupKey from TypeParams comparison
        self.create_index(client, collection_name, index_params)
        idx_names_after, _ = self.list_indexes(client, collection_name, field_name=default_vector_field_name)
        assert len(idx_names_after) == 1
        assert idx_names == idx_names_after
        # 8. cleanup
        self.drop_collection(client, collection_name)

    @pytest.mark.tags(CaseLabel.L1)
    def test_milvus_client_create_index_idempotent_with_collection_warmup(self):
        """
        target: test create index idempotency when warmup is set at collection level
        method: 1. create collection
                2. release collection
                3. alter collection properties to set warmup.vectorField=sync
                4. drop existing index
                5. create index (first time)
                6. create same index again (second time - should be idempotent)
        expected: both create_index calls should succeed (idempotent behavior)
        note: This test verifies the fix for checkParams function in index_meta.go
              using collection-level warmup settings (warmup.vectorField)
        """
        client = self._client()
        collection_name = cf.gen_collection_name_by_testcase_name()
        # 1. create collection
        self.create_collection(client, collection_name, default_dim, consistency_level="Strong")
        # 2. release collection before altering properties
        self.release_collection(client, collection_name)
        # 3. alter collection properties to set warmup.vectorField=sync
        self.alter_collection_properties(client, collection_name, properties={"warmup.vectorField": "sync"})
        # 4. drop existing index
        self.drop_index(client, collection_name, default_vector_field_name)
        res = self.list_indexes(client, collection_name)[0]
        assert res == []
        # 5. prepare index params
        index_params = self.prepare_index_params(client)[0]
        index_params.add_index(
            field_name=default_vector_field_name,
            index_type="HNSW",
            metric_type="L2",
            params={"M": 8, "efConstruction": 200},
        )
        # 6. create index (first time) - should succeed
        self.create_index(client, collection_name, index_params)
        idx_names, _ = self.list_indexes(client, collection_name, field_name=default_vector_field_name)
        assert len(idx_names) == 1
        # 7. create same index again (second time) - should be idempotent and succeed
        self.create_index(client, collection_name, index_params)
        idx_names_after, _ = self.list_indexes(client, collection_name, field_name=default_vector_field_name)
        assert len(idx_names_after) == 1
        assert idx_names == idx_names_after
        # 8. cleanup
        self.drop_collection(client, collection_name)


class TestMilvusClientAlterCollection(TestMilvusClientV2Base):
    @pytest.mark.tags(CaseLabel.L0)
    def test_milvus_client_alter_collection_default(self):
        """
        target: test alter collection
        method:
            1. alter collection properties after load
            verify alter successfully if trying to altering mmap.enabled or collection.ttl.seconds
            2. alter collection properties after release
            verify alter successfully
            3. drop collection properties after load
            verify drop successfully
        expected: alter successfully
        """
        client = self._client()
        collection_name = cf.gen_collection_name_by_testcase_name()
        self.create_collection(client, collection_name, ct.default_dim, consistency_level="Strong")
        self.load_collection(client, collection_name)
        res1 = self.describe_collection(client, collection_name)[0]
        assert len(res1.get("properties", {})) >= 1
        # 1. alter collection properties after load
        self.load_collection(client, collection_name)
        error = {ct.err_code: 999, ct.err_msg: "can not alter mmap properties if collection loaded"}
        self.alter_collection_properties(
            client, collection_name, properties={"mmap.enabled": True}, check_task=CheckTasks.err_res, check_items=error
        )
        error = {ct.err_code: 999, ct.err_msg: "can not delete mmap properties if collection loaded"}
        self.drop_collection_properties(
            client, collection_name, property_keys=["mmap.enabled"], check_task=CheckTasks.err_res, check_items=error
        )
        # TODO
        error = {ct.err_code: 999, ct.err_msg: "cannot delete key dynamicfield.enabled"}
        self.drop_collection_properties(
            client,
            collection_name,
            property_keys=["dynamicfield.enabled"],
            check_task=CheckTasks.err_res,
            check_items=error,
        )
        self.describe_collection(client, collection_name)[0]
        self.drop_collection_properties(client, collection_name, property_keys=["collection.ttl.seconds"])
        # 2. alter collection properties after release
        self.release_collection(client, collection_name)
        self.alter_collection_properties(client, collection_name, properties={"mmap.enabled": True})
        res2 = self.describe_collection(client, collection_name)[0]
        assert {"mmap.enabled": "True"}.items() <= res2.get("properties", {}).items()
        self.alter_collection_properties(client, collection_name, properties={"collection.ttl.seconds": 100})
        res2 = self.describe_collection(client, collection_name)[0]
        assert {"mmap.enabled": "True", "collection.ttl.seconds": "100"}.items() <= res2.get("properties", {}).items()
        self.drop_collection_properties(
            client, collection_name, property_keys=["mmap.enabled", "collection.ttl.seconds"]
        )
        res3 = self.describe_collection(client, collection_name)[0]
        assert "mmap.enabled" not in res3.get("properties", {})
        assert "collection.ttl.seconds" not in res3.get("properties", {})

    @pytest.mark.tags(CaseLabel.L1)
    def test_milvus_client_alter_enable_dynamic_collection_field(self):
        """
        target: test enable dynamic field and mixed field operations
        method: create collection, add field, enable dynamic field, insert mixed data, query/search
        expected: dynamic field works with new field and static field
        """
        client = self._client()
        collection_name = cf.gen_collection_name_by_testcase_name()
        dim = 8
        # 1. create collection
        schema = self.create_schema(client, enable_dynamic_field=False)[0]
        schema.add_field(default_primary_key_field_name, DataType.INT64, max_length=64, is_primary=True, auto_id=False)
        schema.add_field(default_vector_field_name, DataType.FLOAT_VECTOR, dim=dim)
        schema.add_field(default_string_field_name, DataType.VARCHAR, max_length=64, is_partition_key=True)
        index_params = self.prepare_index_params(client)[0]
        index_params.add_index(default_vector_field_name, metric_type="COSINE")
        self.create_collection(client, collection_name, dimension=dim, schema=schema, index_params=index_params)
        # 2. Prepare and insert data
        schema_info = self.describe_collection(client, collection_name)[0]
        rows = cf.gen_row_data_by_schema(nb=default_nb, schema=schema_info)
        results = self.insert(client, collection_name, rows)[0]
        assert results["insert_count"] == default_nb
        # 3. add new field
        default_value = 100
        self.add_collection_field(
            client,
            collection_name,
            field_name="field_new",
            data_type=DataType.INT64,
            nullable=True,
            default_value=default_value,
        )
        # 4. alter collection dynamic field enable
        self.alter_collection_properties(client, collection_name, {"dynamicfield.enabled": True})
        res = self.describe_collection(client, collection_name)[0]
        assert res.get("enable_dynamic_field", None) is True
        # 5. insert data with dynamic field and new field
        vectors = cf.gen_vectors(default_nb, dim, vector_data_type=DataType.FLOAT_VECTOR)
        rows_new = [
            {
                default_primary_key_field_name: i,
                default_vector_field_name: vectors[i],
                default_string_field_name: str(i),
                default_new_field_name: i,
                default_dynamic_field_name: {"a": {"b": i}},
            }
            for i in range(default_nb)
        ]
        self.insert(client, collection_name, rows_new)
        # 6. create index
        index_params = self.prepare_index_params(client)[0]
        index_params.add_index(
            field_name=default_dynamic_field_name,
            index_type="INVERTED",
            params={"json_cast_type": "DOUBLE", "json_path": f"{default_dynamic_field_name}['a']['b']"},
        )
        self.create_index(client, collection_name, index_params)
        index_name = "$meta/" + default_dynamic_field_name
        self.describe_index(
            client,
            collection_name,
            index_name + "/a/b",
            check_task=CheckTasks.check_describe_index_property,
            check_items={
                "json_cast_type": "DOUBLE",
                "json_path": f"{default_dynamic_field_name}['a']['b']",
                "index_type": "INVERTED",
                "field_name": default_dynamic_field_name,
                "index_name": index_name + "/a/b",
            },
        )
        # 7. query using filter with dynamic field and new field
        res = self.query(
            client,
            collection_name,
            filter=f"{default_dynamic_field_name}['a']['b'] >= 0 and field_new < {default_value}",
            output_fields=[default_dynamic_field_name],
            check_task=CheckTasks.check_query_results,
            check_items={
                exp_res: [
                    {"id": item["id"], default_dynamic_field_name: item[default_dynamic_field_name]}
                    for item in rows_new
                ]
            },
        )[0]
        assert set(res[0].keys()) == {default_dynamic_field_name, default_primary_key_field_name}
        # 8. search using filter with dynamic field and new field
        vectors_to_search = [vectors[0]]
        insert_ids = [i for i in range(default_nb)]
        self.search(
            client,
            collection_name,
            vectors_to_search,
            filter=f"{default_dynamic_field_name}['a']['b'] >= 0 and field_new < {default_value}",
            check_task=CheckTasks.check_search_results,
            check_items={
                "enable_milvus_client_api": True,
                "nq": len(vectors_to_search),
                "ids": insert_ids,
                "pk_name": default_primary_key_field_name,
                "limit": default_limit,
            },
        )
        # 9. add new field same as dynamic field name
        self.add_collection_field(
            client,
            collection_name,
            field_name=default_dynamic_field_name,
            data_type=DataType.INT64,
            nullable=True,
            default_value=default_value,
        )
        # 10. query using filter with dynamic field and new field
        res = self.query(
            client,
            collection_name,
            filter=f'$meta["{default_dynamic_field_name}"]["a"]["b"] >= 0 and {default_dynamic_field_name} == {default_value}',
            output_fields=[default_dynamic_field_name, f'$meta["{default_dynamic_field_name}"]'],
            check_task=CheckTasks.check_query_results,
            check_items={exp_res: [{"id": item["id"], default_dynamic_field_name: default_value} for item in rows_new]},
        )[0]
        # dynamic field same as new field name, output_fields contain dynamic field, result do not contain dynamic field
        # https://github.com/milvus-io/milvus/issues/41702
        assert set(res[0].keys()) == {default_dynamic_field_name, default_primary_key_field_name}
        # 11. search using filter with dynamic field and new field
        self.search(
            client,
            collection_name,
            vectors_to_search,
            filter=f'$meta["{default_dynamic_field_name}"]["a"]["b"] >= 0 and {default_dynamic_field_name} == {default_value}',
            check_task=CheckTasks.check_search_results,
            check_items={
                "enable_milvus_client_api": True,
                "nq": len(vectors_to_search),
                "ids": insert_ids,
                "pk_name": default_primary_key_field_name,
                "limit": default_limit,
            },
        )

    @pytest.mark.tags(CaseLabel.L2)
    @pytest.mark.parametrize("old_dynamic_flag, new_dynamic_flag", [(True, True), (False, False)])
    def test_milvus_client_alter_dynamic_collection_field_no_op(self, old_dynamic_flag, new_dynamic_flag):
        """
        target: test dynamic field no-op alter operations
        method: create collection with dynamic flag, alter to same flag, verify unchanged
        expected: no-op alter succeeds without state change
        """
        client = self._client()
        collection_name = cf.gen_collection_name_by_testcase_name()
        dim = 8
        # 1. create collection
        schema = self.create_schema(client, enable_dynamic_field=old_dynamic_flag)[0]
        schema.add_field(default_primary_key_field_name, DataType.INT64, max_length=64, is_primary=True, auto_id=False)
        schema.add_field(default_vector_field_name, DataType.FLOAT_VECTOR, dim=dim)
        schema.add_field(default_string_field_name, DataType.VARCHAR, max_length=64, is_partition_key=True)
        index_params = self.prepare_index_params(client)[0]
        index_params.add_index(default_vector_field_name, metric_type="COSINE")
        self.create_collection(client, collection_name, dimension=dim, schema=schema, index_params=index_params)
        # 2. alter collection dynamic field
        self.alter_collection_properties(client, collection_name, properties={"dynamicfield.enabled": new_dynamic_flag})
        res = self.describe_collection(client, collection_name)[0]
        assert res.get("enable_dynamic_field", None) is new_dynamic_flag

    @pytest.mark.tags(CaseLabel.L1)
    @pytest.mark.parametrize("pk_field_type", [DataType.INT64, DataType.VARCHAR])
    def test_milvus_client_alter_allow_insert_auto_id(self, pk_field_type):
        """
        target: test alter collection allow insert auto id
        method:
            1. create collection with auto_id=True
            2. try to insert data with primary key
            3. verify insert failed
            4. alter collection allow_insert_auto_id=True
            5. insert data with customized primary key
            6. verify insert successfully
            7. verify the new inserted data's primary keys are customized
            8. verify the collection info
            9. drop the collection properties allow_insert_auto_id
            10. alter collection allow_insert_auto_id=False
            11. verify the collection info
            12. alter collection allow_insert_auto_id=True with string value
            13. verify the collection info
            14. insert data with customized primary key
            15. verify insert successfully
        expected: insert successfully
        """
        client = self._client()
        collection_name = cf.gen_collection_name_by_testcase_name()
        dim = 8
        # 1. create collection
        schema = self.create_schema(client, enable_dynamic_field=False)[0]
        schema.add_field(default_primary_key_field_name, pk_field_type, max_length=64, is_primary=True, auto_id=True)
        schema.add_field(default_vector_field_name, DataType.FLOAT_VECTOR, dim=dim)
        index_params = self.prepare_index_params(client)[0]
        index_params.add_index(default_vector_field_name, metric_type="COSINE")
        self.create_collection(client, collection_name, dimension=dim, schema=schema, index_params=index_params)
        # 2. try to insert data with primary key
        rows_with_pk = [
            {
                default_primary_key_field_name: i,
                default_vector_field_name: cf.gen_vectors(1, dim, vector_data_type=DataType.FLOAT_VECTOR)[0],
            }
            for i in range(100)
        ]
        if pk_field_type == DataType.VARCHAR:
            rows_with_pk = [
                {
                    default_primary_key_field_name: f"id_{i}",
                    default_vector_field_name: cf.gen_vectors(1, dim, vector_data_type=DataType.FLOAT_VECTOR)[0],
                }
                for i in range(100)
            ]
        error = {ct.err_code: 999, ct.err_msg: "more fieldData has pass in"}
        self.insert(client, collection_name, rows_with_pk, check_task=CheckTasks.err_res, check_items=error)

        rows_without_pk = cf.gen_row_data_by_schema(nb=100, schema=schema)
        self.insert(client, collection_name, rows_without_pk)
        self.flush(client, collection_name)
        num_entities = self.get_collection_stats(client, collection_name)[0]
        assert num_entities.get("row_count", None) == 100

        self.load_collection(client, collection_name)

        filter = f"{default_primary_key_field_name} in [10, 20,90]"
        if pk_field_type == DataType.VARCHAR:
            filter = f"{default_primary_key_field_name} in ['id_10', 'id_20', 'id_90']"
        res = self.query(client, collection_name, filter=filter, output_fields=[default_primary_key_field_name])[0]
        assert (len(res)) == 0

        # 3. alter collection allow_insert_auto_id=True
        self.alter_collection_properties(client, collection_name, properties={"allow_insert_auto_id": True})
        # 4. insert data with customized primary key
        self.insert(client, collection_name, rows_with_pk)
        # 5. verify insert successfully
        self.flush(client, collection_name)
        num_entities = self.get_collection_stats(client, collection_name)[0]
        assert num_entities.get("row_count", None) == 100 * 2
        # 6. verify the new inserted data's primary keys are customized
        res = self.query(client, collection_name, filter=filter, output_fields=[default_primary_key_field_name])[0]
        assert (len(res)) == 3

        # check the collection info
        res = self.describe_collection(client, collection_name)[0]
        assert res.get("properties").get("allow_insert_auto_id", None) == "True"

        # drop the collection properties allow_insert_auto_id
        self.drop_collection_properties(client, collection_name, property_keys=["allow_insert_auto_id"])
        res = self.describe_collection(client, collection_name)[0]
        assert res.get("properties").get("allow_insert_auto_id", None) is None
        self.insert(client, collection_name, rows_with_pk, check_task=CheckTasks.err_res, check_items=error)

        # alter collection allow_insert_auto_id=False
        self.alter_collection_properties(client, collection_name, properties={"allow_insert_auto_id": False})
        res = self.describe_collection(client, collection_name)[0]
        assert res.get("properties").get("allow_insert_auto_id", None) == "False"
        self.insert(client, collection_name, rows_with_pk, check_task=CheckTasks.err_res, check_items=error)

        # alter collection allow_insert_auto_id=True with string value
        self.alter_collection_properties(client, collection_name, properties={"allow_insert_auto_id": "True"})
        res = self.describe_collection(client, collection_name)[0]
        assert res.get("properties").get("allow_insert_auto_id", None) == "True"
        rows_with_pk = [
            {
                default_primary_key_field_name: i,
                default_vector_field_name: cf.gen_vectors(1, dim, vector_data_type=DataType.FLOAT_VECTOR)[0],
            }
            for i in range(100, 200)
        ]
        if pk_field_type == DataType.VARCHAR:
            rows_with_pk = [
                {
                    default_primary_key_field_name: f"id_{i}",
                    default_vector_field_name: cf.gen_vectors(1, dim, vector_data_type=DataType.FLOAT_VECTOR)[0],
                }
                for i in range(100, 200)
            ]
        self.insert(client, collection_name, rows_with_pk)
        self.flush(client, collection_name)
        num_entities = self.get_collection_stats(client, collection_name)[0]
        assert num_entities.get("row_count", None) == 100 * 3


class TestMilvusClientAlterCollectionField(TestMilvusClientV2Base):
    @pytest.mark.tags(CaseLabel.L0)
    @pytest.mark.parametrize("add_field", [True, False])
    def test_milvus_client_alter_collection_field_default(self, add_field):
        """
        target: test alter collection field before load
        method: alter varchar field max length
        expected: alter successfully
        """
        client = self._client()
        collection_name = cf.gen_collection_name_by_testcase_name()
        # 1. create collection
        schema = self.create_schema(client, enable_dynamic_field=False)[0]
        dim = 32
        pk_field_name = "id_string"
        vector_field_name = "embeddings"
        str_field_name = "title"
        json_field_name = "json_field"
        array_field_name = "tags"
        new_field_name = "field_new"
        max_length = 16
        schema.add_field(pk_field_name, DataType.VARCHAR, max_length=max_length, is_primary=True, auto_id=False)
        schema.add_field(vector_field_name, DataType.FLOAT_VECTOR, dim=dim, mmap_enabled=True)
        schema.add_field(str_field_name, DataType.VARCHAR, max_length=max_length, mmap_enabled=True)
        schema.add_field(json_field_name, DataType.JSON, mmap_enabled=False)
        schema.add_field(
            field_name=array_field_name,
            datatype=DataType.ARRAY,
            element_type=DataType.VARCHAR,
            max_capacity=10,
            max_length=max_length,
        )

        index_params = self.prepare_index_params(client)[0]
        index_params.add_index(
            field_name=vector_field_name, metric_type="COSINE", index_type="IVF_FLAT", params={"nlist": 128}
        )
        index_params.add_index(field_name=str_field_name)
        self.create_collection(client, collection_name, schema=schema, index_params=index_params)
        check_items = {
            str_field_name: {"max_length": max_length, "mmap_enabled": True},
            vector_field_name: {"mmap_enabled": True},
            json_field_name: {"mmap_enabled": False},
        }
        if add_field:
            self.add_collection_field(
                client,
                collection_name,
                field_name="field_new",
                data_type=DataType.VARCHAR,
                nullable=True,
                max_length=max_length,
            )
            check_items["field_new"] = {"max_length": max_length}
        self.describe_collection(
            client, collection_name, check_task=CheckTasks.check_collection_fields_properties, check_items=check_items
        )

        rng = np.random.default_rng(seed=19530)
        rows = [
            {
                pk_field_name: f"id_{i}",
                vector_field_name: list(rng.random((1, dim))[0]),
                str_field_name: cf.gen_str_by_length(max_length),
                json_field_name: {"number": i},
                array_field_name: [cf.gen_str_by_length(max_length) for _ in range(10)],
                # add new field data (only when add_field is True)
                **({"field_new": cf.gen_str_by_length(max_length)} if add_field else {}),
            }
            for i in range(ct.default_nb)
        ]
        self.insert(client, collection_name, rows)

        # 1. alter collection field before load
        self.release_collection(client, collection_name)
        new_max_length = max_length // 2
        # TODO: use one format of mmap_enabled after #38443 fixed
        self.alter_collection_field(
            client,
            collection_name,
            field_name=str_field_name,
            field_params={"max_length": new_max_length, "mmap.enabled": False},
        )
        self.alter_collection_field(
            client, collection_name, field_name=pk_field_name, field_params={"max_length": new_max_length}
        )
        self.alter_collection_field(
            client, collection_name, field_name=json_field_name, field_params={"mmap.enabled": True}
        )
        self.alter_collection_field(
            client, collection_name, field_name=vector_field_name, field_params={"mmap.enabled": False}
        )
        self.alter_collection_field(
            client, collection_name, field_name=array_field_name, field_params={"max_length": new_max_length}
        )
        self.alter_collection_field(
            client, collection_name, field_name=array_field_name, field_params={"max_capacity": 20}
        )
        error = {ct.err_code: 999, ct.err_msg: "can not modify the maxlength for non-string types"}
        self.alter_collection_field(
            client,
            collection_name,
            field_name=vector_field_name,
            field_params={"max_length": new_max_length},
            check_task=CheckTasks.err_res,
            check_items=error,
        )
        error = {ct.err_code: 999, ct.err_msg: "element_type does not allow update in collection field param"}
        self.alter_collection_field(
            client,
            collection_name,
            field_name=array_field_name,
            field_params={"element_type": DataType.INT64},
            check_task=CheckTasks.err_res,
            check_items=error,
        )
        check_items_new = {
            str_field_name: {"max_length": new_max_length, "mmap_enabled": False},
            vector_field_name: {"mmap_enabled": False},
            json_field_name: {"mmap_enabled": True},
            array_field_name: {"max_length": new_max_length, "max_capacity": 20},
        }
        if add_field:
            self.alter_collection_field(
                client, collection_name, field_name="field_new", field_params={"max_length": new_max_length}
            )
            check_items_new["field_new"] = {"max_length": new_max_length}
        self.describe_collection(
            client,
            collection_name,
            check_task=CheckTasks.check_collection_fields_properties,
            check_items=check_items_new,
        )
        # verify that cannot insert data with the old max_length
        fields_to_verify = [pk_field_name, str_field_name, array_field_name]
        if add_field:
            fields_to_verify.append(new_field_name)
        for alter_field in fields_to_verify:
            error = {ct.err_code: 999, ct.err_msg: f"length of varchar field {alter_field} exceeds max length"}
            if alter_field == array_field_name:
                error = {
                    ct.err_code: 999,
                    ct.err_msg: f'length of Array array field "{array_field_name}" exceeds max length',
                }
            rows = [
                {
                    pk_field_name: cf.gen_str_by_length(max_length) if alter_field == pk_field_name else f"id_{i}",
                    vector_field_name: list(rng.random((1, dim))[0]),
                    str_field_name: cf.gen_str_by_length(max_length) if alter_field == str_field_name else f"ti_{i}",
                    json_field_name: {"number": i},
                    array_field_name: [cf.gen_str_by_length(max_length) for _ in range(10)]
                    if alter_field == array_field_name
                    else [f"tags_{j}" for j in range(10)],
                    **(
                        {"field_new": cf.gen_str_by_length(max_length)}
                        if add_field and alter_field == new_field_name
                        else {}
                    ),
                }
                for i in range(ct.default_nb, ct.default_nb + 10)
            ]
            self.insert(client, collection_name, rows, check_task=CheckTasks.err_res, check_items=error)

        # verify that can insert data with the new max_length
        rows = [
            {
                pk_field_name: f"new_{cf.gen_str_by_length(new_max_length - 4)}",
                vector_field_name: list(rng.random((1, dim))[0]),
                str_field_name: cf.gen_str_by_length(new_max_length),
                json_field_name: {"number": i},
                array_field_name: [cf.gen_str_by_length(new_max_length) for _ in range(10)],
                **({"field_new": cf.gen_str_by_length(new_max_length)} if add_field else {}),
            }
            for i in range(ct.default_nb, ct.default_nb + 10)
        ]
        self.insert(client, collection_name, rows)

        # 2. alter collection field after load
        self.load_collection(client, collection_name)
        error = {ct.err_code: 999, ct.err_msg: "can not alter collection field properties if collection loaded"}
        self.alter_collection_field(
            client,
            collection_name,
            field_name=str_field_name,
            field_params={"max_length": max_length, "mmap.enabled": True},
            check_task=CheckTasks.err_res,
            check_items=error,
        )
        self.alter_collection_field(
            client,
            collection_name,
            field_name=vector_field_name,
            field_params={"mmap.enabled": True},
            check_task=CheckTasks.err_res,
            check_items=error,
        )
        self.alter_collection_field(
            client, collection_name, field_name=pk_field_name, field_params={"max_length": max_length}
        )
        if add_field:
            self.alter_collection_field(
                client, collection_name, field_name=new_field_name, field_params={"max_length": max_length}
            )
        res = self.query(client, collection_name, filter=f"{pk_field_name} in ['id_10', 'id_20']", output_fields=["*"])[
            0
        ]
        assert (len(res)) == 2
        res = self.query(client, collection_name, filter=f"{pk_field_name} like 'new_%'", output_fields=["*"])[0]
        assert (len(res)) == 10

    @pytest.mark.tags(CaseLabel.L1)
    def test_milvus_client_alter_collection_field_nullable_field(self):
        """
        target: test alter collection field with nullable field
        method: create collection with nullable field and alter field
        expected: alter successfully
        """
        client = self._client()
        collection_name = cf.gen_collection_name_by_testcase_name()
        dim = 8
        # create collection
        schema = self.create_schema(client, enable_dynamic_field=False)[0]
        schema.add_field("id_string", DataType.VARCHAR, max_length=64, is_primary=True, auto_id=False)
        schema.add_field("embeddings_1", DataType.FLOAT_VECTOR, dim=dim, nullable=True)
        schema.add_field("embeddings_2", DataType.FLOAT_VECTOR, dim=dim)
        schema.add_field("varchar_1", DataType.VARCHAR, max_length=64, nullable=True)
        schema.add_field("varchar_2", DataType.VARCHAR, max_length=64)
        self.create_collection(client, collection_name, dimension=dim, schema=schema)

        # try to alert nullable vector field to non-nullable field
        error = {ct.err_code: 999, ct.err_msg: "nullable does not allow update in collection field param"}
        self.alter_collection_field(
            client,
            collection_name,
            field_name="embeddings_1",
            field_params={"nullable": False},
            check_task=CheckTasks.err_res,
            check_items=error,
        )
        # try to alert non-nullable vector field to nullable field
        self.alter_collection_field(
            client,
            collection_name,
            field_name="embeddings_2",
            field_params={"nullable": True},
            check_task=CheckTasks.err_res,
            check_items=error,
        )
        # try to alert nullable varchar field to non-nullable varchar field
        self.alter_collection_field(
            client,
            collection_name,
            field_name="varchar_1",
            field_params={"nullable": False},
            check_task=CheckTasks.err_res,
            check_items=error,
        )
        # try to alert non-nullable varchar field to nullable varchar field
        self.alter_collection_field(
            client,
            collection_name,
            field_name="varchar_2",
            field_params={"nullable": True},
            check_task=CheckTasks.err_res,
            check_items=error,
        )

        # add a nullable vector field to the collection
        self.add_collection_field(
            client, collection_name, field_name="embeddings_3", data_type=DataType.FLOAT_VECTOR, dim=dim, nullable=True
        )
        # try to alert the new added nullable vector field to non-nullable field
        self.alter_collection_field(
            client,
            collection_name,
            field_name="embeddings_3",
            field_params={"nullable": False},
            check_task=CheckTasks.err_res,
            check_items=error,
        )
        # add a nullable varchar field to the collection
        self.add_collection_field(
            client, collection_name, field_name="varchar_3", data_type=DataType.VARCHAR, max_length=64, nullable=True
        )
        # try to alert the new added nullable varchar field to non-nullable varchar field
        self.alter_collection_field(
            client,
            collection_name,
            field_name="varchar_3",
            field_params={"nullable": False},
            check_task=CheckTasks.err_res,
            check_items=error,
        )

        # drop the collection
        self.drop_collection(client, collection_name)


class TestMilvusClientAlterDatabase(TestMilvusClientV2Base):
    @pytest.mark.tags(CaseLabel.L0)
    def test_milvus_client_alter_database_default(self):
        """
        target: test alter database
        method:
            1. alter database properties before load
            alter successfully
            2. alter database properties after load
            alter successfully
        expected: alter successfully
        """
        client = self._client()
        collection_name = cf.gen_collection_name_by_testcase_name()
        self.create_collection(client, collection_name, ct.default_dim, consistency_level="Strong")
        self.release_collection(client, collection_name)
        default_db = "default"
        res1 = self.describe_database(client, db_name=default_db)[0]
        if len(res1.keys()) != 1:
            self.drop_database_properties(client, db_name=default_db, property_keys=res1.keys())
        assert len(self.describe_database(client, default_db)[0].keys()) == 1
        for need_load in [True, False]:
            if need_load:
                log.debug("alter database after load collection")
                self.load_collection(client, collection_name)

            # 1. alter default database properties before load
            properties = {
                "key1": 1,
                "key2": "value2",
                "key3": [1, 2, 3],
            }
            self.alter_database_properties(client, db_name=default_db, properties=properties)
            res1 = self.describe_database(client, db_name=default_db)[0]
            # assert res1.properties.items() >= properties.items()
            assert len(res1.keys()) == 4
            my_db = cf.gen_unique_str(prefix)
            self.create_database(client, my_db, properties=properties)
            res1 = self.describe_database(client, db_name=my_db)[0]
            # assert res1.properties.items() >= properties.items()
            assert len(res1.keys()) == 4
            properties = {"key1": 2, "key2": "value3", "key3": [1, 2, 3], "key4": 0.123}
            self.alter_database_properties(client, db_name=my_db, properties=properties)
            res1 = self.describe_database(client, db_name=my_db)[0]
            # assert res1.properties.items() >= properties.items()
            assert len(res1.keys()) == 5

            # drop the default database properties
            self.drop_database_properties(client, db_name=default_db, property_keys=["key1", "key2"])
            res1 = self.describe_database(client, db_name=default_db)[0]
            assert len(res1.keys()) == 2
            self.drop_database_properties(client, db_name=default_db, property_keys=["key3", "key_non_exist"])
            res1 = self.describe_database(client, db_name=default_db)[0]
            assert len(res1.keys()) == 1
            # drop the user database
            self.drop_database(client, my_db)


class TestMilvusClientAlterCollectionFieldDescriptionValid(TestMilvusClientV2Base):
    """
    Positive tests for alter_collection_field() to change field description
    PR: https://github.com/milvus-io/milvus/pull/47057
    Issue: https://github.com/milvus-io/milvus/issues/46896
    """

    @pytest.mark.tags(CaseLabel.L0)
    def test_alter_field_description_basic(self):
        """
        target: test basic functionality of altering field description
        method: create collection, alter field description, verify change
        expected: field description updated successfully
        """
        client = self._client()
        collection_name = cf.gen_collection_name_by_testcase_name()

        # 1. Create collection with default schema
        self.create_collection(client, collection_name, default_dim)

        # 2. Alter field description
        new_description = "This is a new description for vector field"
        self.alter_collection_field(
            client,
            collection_name,
            field_name=default_vector_field_name,
            field_params={"field.description": new_description},
        )

        # 3. Verify description changed
        desc_res = self.describe_collection(client, collection_name)[0]
        for field in desc_res.get("fields", []):
            if field.get("name") == default_vector_field_name:
                assert field.get("description") == new_description, (
                    f"Expected description '{new_description}', got '{field.get('description')}'"
                )
                break

        # 4. Cleanup
        self.drop_collection(client, collection_name)

    @pytest.mark.tags(CaseLabel.L1)
    def test_alter_primary_key_field_description(self):
        """
        target: test altering primary key field description
        method: alter the description of primary key field
        expected: description updated successfully
        """
        client = self._client()
        collection_name = cf.gen_collection_name_by_testcase_name()

        # 1. Create collection
        self.create_collection(client, collection_name, default_dim)

        # 2. Alter primary key field description
        new_description = "Primary key field for entity identification"
        self.alter_collection_field(
            client,
            collection_name,
            field_name=default_primary_key_field_name,
            field_params={"field.description": new_description},
        )

        # 3. Verify
        desc_res = self.describe_collection(client, collection_name)[0]
        for field in desc_res.get("fields", []):
            if field.get("name") == default_primary_key_field_name:
                assert field.get("description") == new_description
                break

        # 4. Cleanup
        self.drop_collection(client, collection_name)

    @pytest.mark.tags(CaseLabel.L1)
    def test_alter_vector_field_description(self):
        """
        target: test altering vector field description
        method: alter the description of vector field
        expected: description updated successfully
        """
        client = self._client()
        collection_name = cf.gen_collection_name_by_testcase_name()

        # 1. Create collection with custom schema
        schema = self.create_schema(client, enable_dynamic_field=True)[0]
        schema.add_field("pk", DataType.INT64, is_primary=True, auto_id=False)
        schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=default_dim, description="original description")
        self.create_collection(client, collection_name, schema=schema)

        # 2. Alter vector field description
        new_description = "Dense embedding vector for similarity search"
        self.alter_collection_field(
            client, collection_name, field_name="embedding", field_params={"field.description": new_description}
        )

        # 3. Verify
        desc_res = self.describe_collection(client, collection_name)[0]
        for field in desc_res.get("fields", []):
            if field.get("name") == "embedding":
                assert field.get("description") == new_description
                break

        # 4. Cleanup
        self.drop_collection(client, collection_name)

    @pytest.mark.tags(CaseLabel.L1)
    def test_alter_scalar_field_description(self):
        """
        target: test altering scalar field description
        method: alter the description of a scalar field (VarChar)
        expected: description updated successfully
        """
        client = self._client()
        collection_name = cf.gen_collection_name_by_testcase_name()

        # 1. Create collection with scalar field
        schema = self.create_schema(client, enable_dynamic_field=True)[0]
        schema.add_field("pk", DataType.INT64, is_primary=True, auto_id=False)
        schema.add_field(default_vector_field_name, DataType.FLOAT_VECTOR, dim=default_dim)
        schema.add_field("title", DataType.VARCHAR, max_length=256, description="original title description")
        self.create_collection(client, collection_name, schema=schema)

        # 2. Alter scalar field description
        new_description = "Title of the document"
        self.alter_collection_field(
            client, collection_name, field_name="title", field_params={"field.description": new_description}
        )

        # 3. Verify
        desc_res = self.describe_collection(client, collection_name)[0]
        for field in desc_res.get("fields", []):
            if field.get("name") == "title":
                assert field.get("description") == new_description
                break

        # 4. Cleanup
        self.drop_collection(client, collection_name)

    @pytest.mark.tags(CaseLabel.L1)
    def test_alter_field_description_multiple_times(self):
        """
        target: test altering field description multiple times
        method: alter the same field description consecutively
        expected: each alteration succeeds, idempotent behavior
        """
        client = self._client()
        collection_name = cf.gen_collection_name_by_testcase_name()

        # 1. Create collection
        self.create_collection(client, collection_name, default_dim)

        # 2. Alter description multiple times
        descriptions = ["First description", "Second description", "Third description", "Final description"]

        for desc in descriptions:
            self.alter_collection_field(
                client, collection_name, field_name=default_vector_field_name, field_params={"field.description": desc}
            )

            # Verify each change
            desc_res = self.describe_collection(client, collection_name)[0]
            for field in desc_res.get("fields", []):
                if field.get("name") == default_vector_field_name:
                    assert field.get("description") == desc
                    break

        # 3. Cleanup
        self.drop_collection(client, collection_name)

    @pytest.mark.tags(CaseLabel.L2)
    def test_alter_field_description_to_empty(self):
        """
        target: test altering field description to empty string
        method: set field description to empty string
        expected: description cleared successfully
        """
        client = self._client()
        collection_name = cf.gen_collection_name_by_testcase_name()

        # 1. Create collection with field having description
        schema = self.create_schema(client, enable_dynamic_field=True)[0]
        schema.add_field("pk", DataType.INT64, is_primary=True, auto_id=False)
        schema.add_field(
            default_vector_field_name,
            DataType.FLOAT_VECTOR,
            dim=default_dim,
            description="This field has a description",
        )
        self.create_collection(client, collection_name, schema=schema)

        # 2. Clear description
        self.alter_collection_field(
            client, collection_name, field_name=default_vector_field_name, field_params={"field.description": ""}
        )

        # 3. Verify description is empty
        desc_res = self.describe_collection(client, collection_name)[0]
        for field in desc_res.get("fields", []):
            if field.get("name") == default_vector_field_name:
                assert field.get("description") == ""
                break

        # 4. Cleanup
        self.drop_collection(client, collection_name)

    @pytest.mark.tags(CaseLabel.L2)
    @pytest.mark.parametrize(
        "description",
        [
            "中文描述测试",
            "Description with emoji 🚀🎉",
            "Line1\nLine2\nLine3",
            "Tab\tseparated\tvalues",
            "Special chars: !@#$%^&*()_+-=[]{}|;':\",./<>?",
            " " * 100,  # Many spaces
        ],
    )
    def test_alter_field_description_special_characters(self, description):
        """
        target: test altering field description with special characters
        method: set description containing unicode, emoji, newlines, etc.
        expected: description updated successfully with special characters preserved
        """
        client = self._client()
        collection_name = cf.gen_collection_name_by_testcase_name()

        # 1. Create collection
        self.create_collection(client, collection_name, default_dim)

        # 2. Alter with special description
        self.alter_collection_field(
            client,
            collection_name,
            field_name=default_vector_field_name,
            field_params={"field.description": description},
        )

        # 3. Verify
        desc_res = self.describe_collection(client, collection_name)[0]
        for field in desc_res.get("fields", []):
            if field.get("name") == default_vector_field_name:
                assert field.get("description") == description
                break

        # 4. Cleanup
        self.drop_collection(client, collection_name)


class TestMilvusClientAlterCollectionFieldDescriptionInvalid(TestMilvusClientV2Base):
    """
    Negative tests for alter_collection_field() to change field description
    PR: https://github.com/milvus-io/milvus/pull/47057
    """

    @pytest.mark.tags(CaseLabel.L1)
    def test_alter_field_description_nonexistent_collection(self):
        """
        target: test altering field description on non-existent collection
        method: call alter_collection_field with non-existent collection name
        expected: raise exception with error code 100
        """
        client = self._client()
        collection_name = cf.gen_collection_name_by_testcase_name()

        # Do not create collection, directly alter
        error = {ct.err_code: 100, ct.err_msg: "collection not found"}

        self.alter_collection_field(
            client,
            collection_name,
            field_name=default_vector_field_name,
            field_params={"field.description": "new description"},
            check_task=CheckTasks.err_res,
            check_items=error,
        )

    @pytest.mark.tags(CaseLabel.L1)
    def test_alter_field_description_nonexistent_field(self):
        """
        target: test altering description of non-existent field
        method: call alter_collection_field with non-existent field name
        expected: raise exception indicating field not found
        """
        client = self._client()
        collection_name = cf.gen_collection_name_by_testcase_name()

        # 1. Create collection
        self.create_collection(client, collection_name, default_dim)

        # 2. Alter non-existent field
        error = {ct.err_code: 1100, ct.err_msg: "does not exist in collection"}

        self.alter_collection_field(
            client,
            collection_name,
            field_name="nonexistent_field",
            field_params={"field.description": "new description"},
            check_task=CheckTasks.err_res,
            check_items=error,
        )

        # 3. Cleanup
        self.drop_collection(client, collection_name)

    @pytest.mark.tags(CaseLabel.L1)
    def test_alter_field_description_empty_collection_name(self):
        """
        target: test altering field description with empty collection name
        method: call alter_collection_field with empty string collection name
        expected: raise exception with error code 1
        """
        client = self._client()
        collection_name = ""

        error = {ct.err_code: 1, ct.err_msg: f"`collection_name` value {collection_name} is illegal"}

        self.alter_collection_field(
            client,
            collection_name,
            field_name=default_vector_field_name,
            field_params={"field.description": "new description"},
            check_task=CheckTasks.err_res,
            check_items=error,
        )

    @pytest.mark.tags(CaseLabel.L1)
    def test_alter_field_description_empty_field_name(self):
        """
        target: test altering field description with empty field name
        method: call alter_collection_field with empty string field name
        expected: raise exception indicating invalid field name
        """
        client = self._client()
        collection_name = cf.gen_collection_name_by_testcase_name()

        # 1. Create collection
        self.create_collection(client, collection_name, default_dim)

        # 2. Alter with empty field name
        error = {ct.err_code: 1100, ct.err_msg: "does not exist in collection"}

        self.alter_collection_field(
            client,
            collection_name,
            field_name="",
            field_params={"field.description": "new description"},
            check_task=CheckTasks.err_res,
            check_items=error,
        )

        # 3. Cleanup
        self.drop_collection(client, collection_name)
