from collections import Counter
from collections.abc import Iterable
from pathlib import Path
from typing import Any, get_args

import pytest

from cognite_toolkit._cdf_tk.commands._migrate.selectors import AssetCentricMigrationSelector
from cognite_toolkit._cdf_tk.storageio import (
    AssetIO,
    CanvasIO,
    ChartIO,
    DatapointsIO,
    EventIO,
    FileContentIO,
    InstanceIO,
    RawIO,
    StorageIO,
    TimeSeriesIO,
    get_upload_io,
)
from cognite_toolkit._cdf_tk.storageio.selectors import (
    AllChartsSelector,
    AssetCentricFileSelector,
    AssetSubtreeSelector,
    CanvasExternalIdSelector,
    ChartExternalIdSelector,
    ChartOwnerSelector,
    DataPointsFileSelector,
    DataSelector,
    DataSetSelector,
    FileDataModelingTemplateSelector,
    FileMetadataTemplateSelector,
    InstanceFileSelector,
    InstanceSpaceSelector,
    InstanceViewSelector,
    RawTableSelector,
    Selector,
    SelectorAdapter,
)
from cognite_toolkit._cdf_tk.utils import humanize_collection
from cognite_toolkit._cdf_tk.utils._auxiliary import get_concrete_subclasses
from cognite_toolkit._cdf_tk.utils.file import read_yaml_file


def example_selector_data() -> Iterable[tuple]:
    yield pytest.param(
        {"type": "rawTable", "table": {"dbName": "my_db", "tableName": "my_table"}},
        RawTableSelector,
        RawIO,
        RawIO.KIND,
        id="RawTableSelector",
    )
    yield pytest.param(
        {
            "type": "instanceView",
            "view": {"space": "my_space", "externalId": "my_view", "version": "v1"},
            "instanceType": "node",
            "instanceSpaces": ["space1", "space2"],
        },
        InstanceViewSelector,
        InstanceIO,
        InstanceIO.KIND,
        id="InstanceViewSelector",
    )
    yield pytest.param(
        {"type": "instanceFile", "datafile": "path/to/file.csv", "validateInstance": True},
        InstanceFileSelector,
        InstanceIO,
        InstanceIO.KIND,
        id="InstanceFileSelector",
    )
    yield pytest.param(
        {"type": "dataSet", "dataSetExternalId": "my_data_set", "kind": "Assets"},
        DataSetSelector,
        AssetIO,
        AssetIO.KIND,
        id="DataSetSelector Assets",
    )
    yield pytest.param(
        {"type": "assetSubtree", "hierarchy": "root/child", "kind": "Assets"},
        AssetSubtreeSelector,
        AssetIO,
        AssetIO.KIND,
        id="AssetSubtreeSelector Assets",
    )
    yield pytest.param(
        {"type": "dataSet", "dataSetExternalId": "my_data_set", "kind": "TimeSeries"},
        DataSetSelector,
        TimeSeriesIO,
        TimeSeriesIO.KIND,
        id="DataSetSelector TimeSeries",
    )
    yield pytest.param(
        {"type": "assetSubtree", "hierarchy": "root/child", "kind": "TimeSeries"},
        AssetSubtreeSelector,
        TimeSeriesIO,
        TimeSeriesIO.KIND,
        id="AssetSubtreeSelector TimeSeries",
    )
    yield pytest.param(
        {"type": "dataSet", "dataSetExternalId": "my_data_set", "kind": "Events"},
        DataSetSelector,
        EventIO,
        EventIO.KIND,
        id="DataSetSelector Events",
    )
    yield pytest.param(
        {"type": "assetSubtree", "hierarchy": "root/child", "kind": "Events"},
        AssetSubtreeSelector,
        EventIO,
        EventIO.KIND,
        id="AssetSubtreeSelector Events",
    )
    yield pytest.param(
        {"type": "chartOwner", "ownerId": "doctrino"},
        ChartOwnerSelector,
        ChartIO,
        ChartIO.KIND,
        id="ChartOwnerSelector",
    )
    yield pytest.param(
        {"type": "allCharts"},
        AllChartsSelector,
        ChartIO,
        ChartIO.KIND,
        id="AllChartSelector",
    )
    yield pytest.param(
        {"type": "assetFile", "datafile": "path/to/file.csv", "kind": "Assets"},
        AssetCentricFileSelector,
        AssetIO,
        AssetIO.KIND,
        id="AssetCentricFileSelector",
    )
    yield pytest.param(
        {
            "type": "instanceSpace",
            "instanceSpace": "myLocation",
            "instanceType": "node",
            "viewId": {
                "space": "my_space",
                "externalId": "MyView",  # Version is optional
            },
        },
        InstanceSpaceSelector,
        InstanceIO,
        InstanceIO.KIND,
        id="InstanceSpaceSelector",
    )
    yield pytest.param(
        {
            "type": "datapointsFile",
            "timestamp_column": "timestamp",
            "columns": [
                {
                    "column_type": "instance",
                    "column": "ts1",
                    "dtype": "numeric",
                    "space": "space1",
                    "external_id": "extId1",
                },
                {
                    "column_type": "externalId",
                    "column": "ts2",
                    "dtype": "string",
                    "external_id": "extId2",
                },
            ],
        },
        DataPointsFileSelector,
        DatapointsIO,
        DatapointsIO.KIND,
        id="DataPointsFileSelector",
    )
    yield pytest.param(
        {
            "type": "chartExternalId",
            "kind": "Charts",
            "externalIds": ["5400ab34-37f1-470d-b965-d81404ac92d8"],
        },
        ChartExternalIdSelector,
        ChartIO,
        ChartIO.KIND,
        id="ChartExternalIdSelector",
    )
    yield pytest.param(
        {
            "type": "canvasExternalId",
            "kind": "IndustrialCanvas",
            "externalIds": ["c1a2b3d4-e5f6-4789-90ab-cdef12345678"],
        },
        CanvasExternalIdSelector,
        CanvasIO,
        CanvasIO.KIND,
        id="CanvasExternalIdSelector",
    )
    yield pytest.param(
        {
            "type": "fileMetadataTemplate",
            "kind": "FileContent",
            "file_directory": "path/to/files",
            "template": {
                "name": "$FILENAME",
                "external_id": "file_$FILENAME",
                "source": "Uploaded via Toolkit",
            },
        },
        FileMetadataTemplateSelector,
        FileContentIO,
        FileContentIO.KIND,
        id="FileMetadataTemplateSelector",
    )
    yield pytest.param(
        {
            "type": "fileDataModelingTemplate",
            "kind": "FileContent",
            "file_directory": "path/to/files",
            "viewId": {
                "space": "cdf_cdm",
                "externalId": "CogniteFile",
                "version": "v1",
            },
            "template": {
                "space": "my_space",
                "external_id": "data_modeling_$FILENAME",
            },
        },
        FileDataModelingTemplateSelector,
        FileContentIO,
        FileContentIO.KIND,
        id="FileDataModelingTemplateSelector",
    )


@pytest.fixture(scope="module")
def all_selectors() -> list[type[DataSelector]]:
    return get_concrete_subclasses(DataSelector)


class TestDataSelectors:
    """Test to ensure all data selectors are working as expected."""

    def test_all_selectors_in_union(self, all_selectors: list[type[DataSelector]]) -> None:
        # The migration selectors are not part of the Selector union, they
        # are only used for migration commands.
        migration_selectors = get_concrete_subclasses(AssetCentricMigrationSelector)
        all_union_selectors = get_args(Selector.__args__[0])
        missing = set(all_selectors) - set(all_union_selectors) - set(migration_selectors)
        assert not missing, (
            f"The following DataSelector subclasses are "
            f"missing from the Selector union: {humanize_collection([cls.__name__ for cls in missing])}"
        )

    def test_all_types_are_unique(self, all_selectors: list[type[DataSelector]]) -> None:
        types = Counter(cls.model_fields["type"].default for cls in all_selectors)
        duplicates = [t for t, count in types.items() if count > 1]
        assert not duplicates, f"The following DataSelector types are not unique: {humanize_collection(duplicates)}"

    def test_example_data_is_complete(self) -> None:
        migration_selectors = get_concrete_subclasses(AssetCentricMigrationSelector)
        # Migration selectors are not part of the Selector union, and are not
        # required to have example data here.
        all_selectors = [cls for cls in get_concrete_subclasses(DataSelector) if cls not in migration_selectors]
        example_types = {p.values[0]["type"] for p in example_selector_data()}
        all_types = {cls.model_fields["type"].default for cls in all_selectors}
        missing = all_types - example_types
        assert not missing, f"The following DataSelector types are missing example data: {humanize_collection(missing)}"

    @pytest.mark.parametrize("data,expected_selector,expected_io,kind", list(example_selector_data()))
    def test_selector_instance(
        self,
        data: dict[str, Any],
        expected_selector: type[DataSelector],
        expected_io: type[StorageIO],
        kind: str,
        tmp_path: Path,
    ) -> None:
        instance = SelectorAdapter.validate_python(data)

        # Assert correct type
        assert isinstance(instance, expected_selector), (
            f"Expected {expected_selector.__name__}, got {type(instance).__name__}"
        )
        # Assert __str__ is implemented
        assert str(instance), f"__str__ not implemented for {type(instance).__name__}"

        # Assert group is implemented
        assert instance.group, f"group property not implemented for {type(instance).__name__}"

        # Assert correct IO type
        assert get_upload_io(instance) is expected_io, (
            f"Expected {expected_io.__name__} for selector {type(instance).__name__}, got {get_upload_io(instance).__name__}"
        )

        # Assert selector is hashable
        assert isinstance(hash(instance), int), f"{type(instance).__name__} is not hashable"

        # Assert serialization/deserialization
        filepath = instance.dump_to_file(tmp_path)
        assert filepath.exists(), f"dump_to_file did not create file for {type(instance).__name__}"

        data = read_yaml_file(filepath)
        loaded = SelectorAdapter.validate_python(data)
        assert loaded.model_dump() == instance.model_dump()
        assert type(loaded) is type(instance)
