"""Tests for the plugin discovery system."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from pycharter.etl_generator._plugins import (
    discover_plugins,
    load_plugins_from_settings,
    reset_discovery,
)
from pycharter.etl_generator.extractors.base import BaseExtractor
from pycharter.etl_generator.extractors.factory import ExtractorFactory
from pycharter.etl_generator.loaders.base import BaseLoader
from pycharter.etl_generator.loaders.factory import LoaderFactory


@pytest.fixture(autouse=True)
def _reset_plugins():
    """Reset plugin discovery state and factory registries between tests."""
    reset_discovery()
    yield
    reset_discovery()
    # Clean up any test registrations
    for name in ("test_ext", "mock_ext", "bad_ext", "kafka", "bigquery"):
        ExtractorFactory.unregister(name)
    for name in ("test_ldr", "mock_ldr", "bad_ldr", "kafka", "bigquery"):
        LoaderFactory.unregister(name)


class TestDiscoverPlugins:
    """Tests for entry-point-based plugin discovery."""

    def test_discover_is_idempotent(self):
        """Calling discover_plugins() twice is a no-op the second time."""
        with patch(
            "pycharter.etl_generator._plugins._discover_entry_points"
        ) as mock_ep:
            discover_plugins()
            discover_plugins()
            mock_ep.assert_called_once()

    def test_discovers_extractor_entry_points(self):
        """Extractor entry points are registered with ExtractorFactory."""
        mock_cls = type(
            "MockExt",
            (BaseExtractor,),
            {
                "extract_streaming": lambda self, *a, **kw: None,
            },
        )
        mock_ep = MagicMock()
        mock_ep.name = "mock_ext"
        mock_ep.load.return_value = mock_cls

        def fake_entry_points(group=""):
            if group == "pycharter.extractors":
                return [mock_ep]
            return []

        with patch(
            "importlib.metadata.entry_points",
            side_effect=fake_entry_points,
        ):
            reset_discovery()
            discover_plugins()

        assert ExtractorFactory._registry.get("mock_ext") is mock_cls

    def test_discovers_loader_entry_points(self):
        """Loader entry points are registered with LoaderFactory."""
        mock_cls = type(
            "MockLdr",
            (BaseLoader,),
            {
                "load": lambda self, *a, **kw: None,
            },
        )
        mock_ep = MagicMock()
        mock_ep.name = "mock_ldr"
        mock_ep.load.return_value = mock_cls

        def fake_entry_points(group=""):
            if group == "pycharter.loaders":
                return [mock_ep]
            return []

        with patch(
            "importlib.metadata.entry_points",
            side_effect=fake_entry_points,
        ):
            reset_discovery()
            discover_plugins()

        assert LoaderFactory._registry.get("mock_ldr") is mock_cls


class TestPluginGracefulDegradation:
    """Tests that bad plugins don't crash the system."""

    def test_import_error_is_logged_not_raised(self):
        """A plugin that fails to import is skipped with a warning."""
        mock_ep = MagicMock()
        mock_ep.name = "bad_ext"
        mock_ep.load.side_effect = ImportError("missing dependency")

        def fake_entry_points(group=""):
            if group == "pycharter.extractors":
                return [mock_ep]
            return []

        with patch(
            "importlib.metadata.entry_points",
            side_effect=fake_entry_points,
        ):
            reset_discovery()
            # Should not raise
            discover_plugins()

        assert "bad_ext" not in ExtractorFactory._registry

    def test_non_subclass_is_skipped(self):
        """A plugin that isn't a BaseExtractor subclass is skipped."""
        mock_ep = MagicMock()
        mock_ep.name = "bad_ext"
        mock_ep.load.return_value = str  # not a BaseExtractor subclass

        def fake_entry_points(group=""):
            if group == "pycharter.extractors":
                return [mock_ep]
            return []

        with patch(
            "importlib.metadata.entry_points",
            side_effect=fake_entry_points,
        ):
            reset_discovery()
            discover_plugins()

        assert "bad_ext" not in ExtractorFactory._registry


class TestPluginConfig:
    """Tests for settings.yaml plugins section."""

    def test_loads_extractors_from_settings(self):
        """Extractors declared in settings plugins are registered."""
        mock_cls = type(
            "TestExt",
            (BaseExtractor,),
            {
                "extract_streaming": lambda self, *a, **kw: None,
            },
        )

        with patch(
            "pycharter.etl_generator._plugins.importlib.import_module"
        ) as mock_import:
            mock_module = MagicMock()
            mock_module.TestExt = mock_cls
            mock_import.return_value = mock_module

            load_plugins_from_settings(
                {
                    "plugins": {
                        "extractors": {"test_ext": "mypackage.ext:TestExt"},
                    }
                }
            )

        assert ExtractorFactory._registry.get("test_ext") is mock_cls

    def test_loads_loaders_from_settings(self):
        """Loaders declared in settings plugins are registered."""
        mock_cls = type(
            "TestLdr",
            (BaseLoader,),
            {
                "load": lambda self, *a, **kw: None,
            },
        )

        with patch(
            "pycharter.etl_generator._plugins.importlib.import_module"
        ) as mock_import:
            mock_module = MagicMock()
            mock_module.TestLdr = mock_cls
            mock_import.return_value = mock_module

            load_plugins_from_settings(
                {
                    "plugins": {
                        "loaders": {"test_ldr": "mypackage.ldr:TestLdr"},
                    }
                }
            )

        assert LoaderFactory._registry.get("test_ldr") is mock_cls

    def test_no_plugins_section_is_noop(self):
        """Settings without a plugins section is a no-op."""
        # Should not raise
        load_plugins_from_settings({})
        load_plugins_from_settings({"name": "test"})

    def test_bad_import_ref_is_logged(self):
        """A bad module:Class reference is skipped with a warning."""
        with patch(
            "pycharter.etl_generator._plugins.importlib.import_module",
            side_effect=ImportError("no such module"),
        ):
            load_plugins_from_settings(
                {
                    "plugins": {
                        "extractors": {"bad_ext": "nonexistent:Foo"},
                    }
                }
            )

        assert "bad_ext" not in ExtractorFactory._registry


class TestFactoryDelegation:
    """Tests that pipeline.py and builder.py create components through factories."""

    def test_pipeline_build_uses_extractor_factory(self):
        """Pipeline._build_from_configs delegates to ExtractorFactory.create."""
        from pycharter.etl_generator.pipeline import Pipeline

        extract_config = {"type": "file", "path": "/tmp/test.json", "format": "json"}
        load_config = {"type": "file", "path": "/tmp/out.json", "format": "json"}

        pipeline = Pipeline._build_from_configs(
            extract_config=extract_config,
            transform_config={},
            load_config=load_config,
            variables=None,
            validate=True,
            name="test",
        )
        assert pipeline.extractor is not None
        assert pipeline.loader is not None

    def test_builder_uses_extractor_factory(self):
        """PipelineBuilder delegates to ExtractorFactory."""
        from pycharter.etl_generator.builder import PipelineBuilder

        builder = PipelineBuilder()
        builder.extract_from("file", path="/tmp/test.json", format="json")
        assert builder._extractor is not None

    def test_builder_uses_loader_factory(self):
        """PipelineBuilder delegates to LoaderFactory."""
        from pycharter.etl_generator.builder import PipelineBuilder

        builder = PipelineBuilder()
        builder.load_to("file", path="/tmp/out.json", format="json")
        assert builder._loader is not None
