"""
Comprehensive test suite for BaseNormalizerStep.

Tests cover:
- Initialization with various configurations
- Default behavior of process_lines and process_row
- Strict mode behavior
- Integration with NormalizationReport
- Subclass implementation patterns
- Iterator behavior and edge cases
- Step registry and factory patterns
"""

import pytest
from typing import Iterator, Dict, Any
from atomingest.normalization.core import (
    NormalizationReport,
    NormalizationStepConfig,
)
from atomingest.normalization.steps import (
    BaseNormalizerStep,
    register_step,
    StepFactory,
    _STEP_REGISTRY,
)


class TestBaseNormalizerStepInitialization:
    """Test initialization and configuration of BaseNormalizerStep."""

    def test_init_with_minimal_params(self):
        """Test initialization with only required parameters."""
        report = NormalizationReport()
        config = {}

        step = BaseNormalizerStep(config=config, report=report)

        assert step.config == config
        assert step.report is report
        assert step.strict is False

    def test_init_with_all_params(self):
        """Test initialization with all parameters including strict mode."""
        report = NormalizationReport()
        config = {"key1": "value1", "key2": 42}

        step = BaseNormalizerStep(config=config, report=report, strict=True)

        assert step.config == config
        assert step.report is report
        assert step.strict is True

    def test_init_with_empty_config(self):
        """Test initialization with empty config dictionary."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        assert step.config == {}
        assert step.report is report

    def test_init_with_complex_config(self):
        """Test initialization with complex nested configuration."""
        report = NormalizationReport()
        config = {
            "enabled": True,
            "options": {"nested": {"values": [1, 2, 3]}},
            "patterns": ["*.csv", "*.txt"],
        }

        step = BaseNormalizerStep(config=config, report=report)

        assert step.config == config
        assert step.config["options"]["nested"]["values"] == [1, 2, 3]

    def test_init_report_is_shared_reference(self):
        """Test that the report object is shared, not copied."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        # Modify report through step
        step.report.add_warning("test warning")

        # Should be reflected in original report
        assert "test warning" in report.warnings


class TestBaseNormalizerStepProcessLines:
    """Test the default process_lines method behavior."""

    def test_process_lines_passthrough_empty_iterator(self):
        """Test that empty iterator is handled correctly."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        lines = iter([])
        result = list(step.process_lines(lines))

        assert result == []

    def test_process_lines_passthrough_single_line(self):
        """Test that single line passes through unchanged."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        lines = iter(["single line\n"])
        result = list(step.process_lines(lines))

        assert result == ["single line\n"]

    def test_process_lines_passthrough_multiple_lines(self):
        """Test that multiple lines pass through unchanged in order."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        input_lines = ["line 1\n", "line 2\n", "line 3\n"]
        result = list(step.process_lines(iter(input_lines)))

        assert result == input_lines

    def test_process_lines_preserves_whitespace(self):
        """Test that whitespace is preserved in default implementation."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        lines = iter(["  spaces  \n", "\ttabs\t\n", "  mixed \t \n"])
        result = list(step.process_lines(lines))

        assert result == ["  spaces  \n", "\ttabs\t\n", "  mixed \t \n"]

    def test_process_lines_preserves_special_chars(self):
        """Test that special characters are preserved."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        lines = iter(
            ["line with, comma\n", 'line with "quotes"\n', "line with 'apostrophes'\n"]
        )
        result = list(step.process_lines(lines))

        assert result == [
            "line with, comma\n",
            'line with "quotes"\n',
            "line with 'apostrophes'\n",
        ]

    def test_process_lines_preserves_unicode(self):
        """Test that unicode characters are preserved."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        lines = iter(["café\n", "naïve\n", "日本語\n", "emoji 🎉\n"])
        result = list(step.process_lines(lines))

        assert result == ["café\n", "naïve\n", "日本語\n", "emoji 🎉\n"]

    def test_process_lines_returns_iterator(self):
        """Test that process_lines returns an iterator, not a list."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        lines = iter(["line 1\n", "line 2\n"])
        result = step.process_lines(lines)

        # Should be an iterator/generator
        assert hasattr(result, "__iter__")
        assert hasattr(result, "__next__")

    def test_process_lines_lazy_evaluation(self):
        """Test that process_lines uses lazy evaluation."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        def counting_iterator():
            for i in range(3):
                yield f"line {i}\n"

        result = step.process_lines(counting_iterator())

        # Should not consume the iterator immediately
        first = next(result)
        assert first == "line 0\n"

        second = next(result)
        assert second == "line 1\n"


class TestBaseNormalizerStepProcessRow:
    """Test the default process_row method behavior."""

    def test_process_row_passthrough_empty_dict(self):
        """Test that empty dictionary passes through unchanged."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        row = {}
        result = step.process_row(row)

        assert result == {}

    def test_process_row_passthrough_single_field(self):
        """Test that single field dictionary passes through unchanged."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        row = {"name": "Alice"}
        result = step.process_row(row)

        assert result == {"name": "Alice"}

    def test_process_row_passthrough_multiple_fields(self):
        """Test that multi-field dictionary passes through unchanged."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        row = {"id": 1, "name": "Alice", "email": "alice@example.com", "active": True}
        result = step.process_row(row)

        assert result == row

    def test_process_row_preserves_types(self):
        """Test that various data types are preserved."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        row = {
            "string": "text",
            "int": 42,
            "float": 3.14,
            "bool": True,
            "none": None,
            "list": [1, 2, 3],
            "dict": {"nested": "value"},
        }
        result = step.process_row(row)

        assert result == row
        assert isinstance(result["int"], int)
        assert isinstance(result["float"], float)
        assert isinstance(result["bool"], bool)
        assert result["none"] is None

    def test_process_row_preserves_whitespace_in_values(self):
        """Test that whitespace in string values is preserved."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        row = {
            "leading": "  value",
            "trailing": "value  ",
            "both": "  value  ",
            "internal": "val  ue",
        }
        result = step.process_row(row)

        assert result == row

    def test_process_row_returns_dict(self):
        """Test that process_row returns a dictionary."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        row = {"key": "value"}
        result = step.process_row(row)

        assert isinstance(result, dict)

    def test_process_row_returns_same_reference(self):
        """Test that by default, the same dictionary reference is returned."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        row = {"key": "value"}
        result = step.process_row(row)

        # Default implementation returns the same object
        assert result is row


class TestBaseNormalizerStepStrictMode:
    """Test behavior in strict mode."""

    def test_strict_mode_enabled_via_init(self):
        """Test that strict mode can be enabled during initialization."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report, strict=True)

        assert step.strict is True

    def test_strict_mode_disabled_by_default(self):
        """Test that strict mode is disabled by default."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        assert step.strict is False

    def test_strict_mode_accessible_in_subclass(self):
        """Test that strict mode flag is accessible in subclasses."""

        class CustomStep(BaseNormalizerStep):
            def process_lines(self, lines: Iterator[str]) -> Iterator[str]:
                if self.strict:
                    # In strict mode, do something different
                    for line in lines:
                        if not line.strip():
                            raise ValueError("Empty line not allowed in strict mode")
                        yield line
                else:
                    yield from lines

        report = NormalizationReport()

        # Non-strict mode
        step_lenient = CustomStep(config={}, report=report, strict=False)
        result = list(step_lenient.process_lines(iter(["line\n", "\n", "another\n"])))
        assert len(result) == 3

        # Strict mode
        step_strict = CustomStep(config={}, report=report, strict=True)
        with pytest.raises(ValueError, match="Empty line not allowed"):
            list(step_strict.process_lines(iter(["line\n", "\n", "another\n"])))


class TestBaseNormalizerStepReportIntegration:
    """Test integration with NormalizationReport."""

    def test_report_accessible_via_attribute(self):
        """Test that report is accessible via instance attribute."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        assert step.report is report

    def test_report_can_be_modified_during_processing(self):
        """Test that report can be modified during step processing."""

        class CountingStep(BaseNormalizerStep):
            def process_lines(self, lines: Iterator[str]) -> Iterator[str]:
                for line in lines:
                    self.report.increment_total_lines()
                    yield line

        report = NormalizationReport()
        step = CountingStep(config={}, report=report)

        list(step.process_lines(iter(["line1\n", "line2\n", "line3\n"])))

        assert report.total_lines_read == 3

    def test_report_warnings_can_be_added(self):
        """Test that warnings can be added to the report."""

        class WarningStep(BaseNormalizerStep):
            def process_lines(self, lines: Iterator[str]) -> Iterator[str]:
                for line in lines:
                    if line.startswith("#"):
                        self.report.add_warning(
                            f"Comment line detected: {line.strip()}"
                        )
                    yield line

        report = NormalizationReport()
        step = WarningStep(config={}, report=report)

        lines = ["# comment\n", "data\n", "# another comment\n"]
        list(step.process_lines(iter(lines)))

        assert len(report.warnings) == 2
        assert "comment" in report.warnings[0].lower()

    def test_report_metadata_can_be_extracted(self):
        """Test that metadata can be extracted and stored in report."""

        class MetadataStep(BaseNormalizerStep):
            def process_lines(self, lines: Iterator[str]) -> Iterator[str]:
                for line in lines:
                    if line.startswith("DATE:"):
                        date_value = line.split(":", 1)[1].strip()
                        self.report.extracted_metadata["date"] = date_value
                    else:
                        yield line

        report = NormalizationReport()
        step = MetadataStep(config={}, report=report)

        lines = ["DATE: 2024-01-15\n", "data1\n", "data2\n"]
        result = list(step.process_lines(iter(lines)))

        assert report.extracted_metadata["date"] == "2024-01-15"
        assert result == ["data1\n", "data2\n"]


class TestBaseNormalizerStepSubclassing:
    """Test patterns for subclassing BaseNormalizerStep."""

    def test_subclass_can_override_process_lines(self):
        """Test that subclass can override process_lines method."""

        class UppercaseStep(BaseNormalizerStep):
            def process_lines(self, lines: Iterator[str]) -> Iterator[str]:
                for line in lines:
                    yield line.upper()

        report = NormalizationReport()
        step = UppercaseStep(config={}, report=report)

        result = list(step.process_lines(iter(["hello\n", "world\n"])))

        assert result == ["HELLO\n", "WORLD\n"]

    def test_subclass_can_override_process_row(self):
        """Test that subclass can override process_row method."""

        class TrimStep(BaseNormalizerStep):
            def process_row(self, row: Dict[str, Any]) -> Dict[str, Any]:
                return {
                    key: value.strip() if isinstance(value, str) else value
                    for key, value in row.items()
                }

        report = NormalizationReport()
        step = TrimStep(config={}, report=report)

        result = step.process_row({"name": "  Alice  ", "age": 30})

        assert result == {"name": "Alice", "age": 30}

    def test_subclass_can_use_config(self):
        """Test that subclass can access configuration."""

        class ConfigurableStep(BaseNormalizerStep):
            def process_lines(self, lines: Iterator[str]) -> Iterator[str]:
                prefix = self.config.get("prefix", "")
                for line in lines:
                    yield f"{prefix}{line}"

        report = NormalizationReport()
        step = ConfigurableStep(config={"prefix": ">> "}, report=report)

        result = list(step.process_lines(iter(["line1\n", "line2\n"])))

        assert result == [">> line1\n", ">> line2\n"]

    def test_subclass_can_filter_lines(self):
        """Test that subclass can filter out lines."""

        class FilterCommentStep(BaseNormalizerStep):
            def process_lines(self, lines: Iterator[str]) -> Iterator[str]:
                for line in lines:
                    if not line.strip().startswith("#"):
                        yield line

        report = NormalizationReport()
        step = FilterCommentStep(config={}, report=report)

        lines = ["# comment\n", "data1\n", "# another comment\n", "data2\n"]
        result = list(step.process_lines(iter(lines)))

        assert result == ["data1\n", "data2\n"]

    def test_subclass_can_modify_and_add_rows(self):
        """Test that subclass can modify row structure."""

        class AddTimestampStep(BaseNormalizerStep):
            def process_row(self, row: Dict[str, Any]) -> Dict[str, Any]:
                result = row.copy()
                result["processed"] = True
                return result

        report = NormalizationReport()
        step = AddTimestampStep(config={}, report=report)

        result = step.process_row({"id": 1, "name": "Alice"})

        assert result == {"id": 1, "name": "Alice", "processed": True}

    def test_subclass_can_skip_rows(self):
        """Test that subclass can conditionally skip rows."""

        class SkipInactiveStep(BaseNormalizerStep):
            def process_row(self, row: Dict[str, Any]) -> Dict[str, Any]:
                # In real implementation, might return None or raise
                # For this test, just demonstrate conditional logic
                if row.get("active", False):
                    return row
                else:
                    # Could add warning to report
                    self.report.add_warning(f"Skipping inactive row: {row.get('id')}")
                    return row  # Base class always returns something

        report = NormalizationReport()
        step = SkipInactiveStep(config={}, report=report)

        step.process_row({"id": 1, "active": False})

        assert len(report.warnings) == 1
        assert "inactive" in report.warnings[0].lower()

    def test_subclass_multiple_inheritance_pattern(self):
        """Test that multiple levels of subclassing work correctly."""

        class Level1Step(BaseNormalizerStep):
            def process_lines(self, lines: Iterator[str]) -> Iterator[str]:
                for line in lines:
                    yield line.strip() + "\n"

        class Level2Step(Level1Step):
            def process_lines(self, lines: Iterator[str]) -> Iterator[str]:
                # Call parent method
                stripped = super().process_lines(lines)
                # Then add own processing
                for line in stripped:
                    yield line.upper()

        report = NormalizationReport()
        step = Level2Step(config={}, report=report)

        result = list(step.process_lines(iter(["  hello  \n", "  world  \n"])))

        assert result == ["HELLO\n", "WORLD\n"]


class TestBaseNormalizerStepEdgeCases:
    """Test edge cases and boundary conditions."""

    def test_process_lines_with_very_long_line(self):
        """Test handling of very long lines."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        long_line = "a" * 10000 + "\n"
        result = list(step.process_lines(iter([long_line])))

        assert len(result[0]) == 10001

    def test_process_lines_with_empty_strings(self):
        """Test handling of empty strings in iterator."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        lines = ["line1\n", "", "line2\n", "", ""]
        result = list(step.process_lines(iter(lines)))

        assert result == lines

    def test_process_row_with_none_values(self):
        """Test handling of None values in row dictionary."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        row = {"id": 1, "name": None, "email": None}
        result = step.process_row(row)

        assert result == row

    def test_process_row_with_empty_string_values(self):
        """Test handling of empty string values."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        row = {"id": "", "name": "", "email": ""}
        result = step.process_row(row)

        assert result == row

    def test_process_lines_generator_exhaustion(self):
        """Test that generator can only be consumed once."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        lines = iter(["line1\n", "line2\n"])
        result_gen = step.process_lines(lines)

        # First consumption
        first_pass = list(result_gen)
        assert len(first_pass) == 2

        # Second consumption should be empty (generator exhausted)
        second_pass = list(result_gen)
        assert len(second_pass) == 0

    def test_config_with_none_value(self):
        """Test that None can be used in config values."""
        report = NormalizationReport()
        config = {"option1": None, "option2": "value"}

        step = BaseNormalizerStep(config=config, report=report)

        assert step.config["option1"] is None
        assert step.config["option2"] == "value"

    def test_multiple_steps_share_same_report(self):
        """Test that multiple steps can share the same report object."""
        report = NormalizationReport()

        step1 = BaseNormalizerStep(config={"name": "step1"}, report=report)
        step2 = BaseNormalizerStep(config={"name": "step2"}, report=report)

        step1.report.add_warning("Warning from step1")
        step2.report.add_warning("Warning from step2")

        assert len(report.warnings) == 2
        assert report.warnings[0] == "Warning from step1"
        assert report.warnings[1] == "Warning from step2"

    def test_process_lines_with_binary_like_strings(self):
        """Test handling of strings that look like binary data."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        lines = ["\\x00\\x01\\x02\n", "normal line\n"]
        result = list(step.process_lines(iter(lines)))

        assert result == lines


class TestBaseNormalizerStepDocumentation:
    """Test that the class follows documented contracts."""

    def test_class_has_docstring(self):
        """Test that BaseNormalizerStep has documentation."""
        assert BaseNormalizerStep.__doc__ is not None
        assert len(BaseNormalizerStep.__doc__.strip()) > 0

    def test_init_has_docstring(self):
        """Test that __init__ method has documentation."""
        assert BaseNormalizerStep.__init__.__doc__ is not None

    def test_process_lines_has_docstring(self):
        """Test that process_lines method has documentation."""
        assert BaseNormalizerStep.process_lines.__doc__ is not None

    def test_process_row_has_docstring(self):
        """Test that process_row method has documentation."""
        assert BaseNormalizerStep.process_row.__doc__ is not None

    def test_documented_attributes_exist(self):
        """Test that all documented attributes are present."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        # Verify expected attributes exist
        assert hasattr(step, "config")
        assert hasattr(step, "report")
        assert hasattr(step, "strict")
        assert hasattr(step, "process_lines")
        assert hasattr(step, "process_row")


class TestStepRegistry:
    """Test the _STEP_REGISTRY and registration mechanism."""

    def setup_method(self):
        """Save the original registry state before each test."""
        self.original_registry = _STEP_REGISTRY.copy()

    def teardown_method(self):
        """Restore the original registry state after each test."""
        _STEP_REGISTRY.clear()
        _STEP_REGISTRY.update(self.original_registry)

    def test_registry_is_dict(self):
        """Test that _STEP_REGISTRY is a dictionary."""
        assert isinstance(_STEP_REGISTRY, dict)

    def test_registry_keys_are_strings(self):
        """Test that registry keys are strings."""

        # Register a test step
        @register_step("TestStep")
        class TestStep(BaseNormalizerStep):
            pass

        for key in _STEP_REGISTRY.keys():
            assert isinstance(key, str)

    def test_registry_values_are_classes(self):
        """Test that registry values are class types."""

        @register_step("TestStep")
        class TestStep(BaseNormalizerStep):
            pass

        for value in _STEP_REGISTRY.values():
            assert isinstance(value, type)
            assert issubclass(value, BaseNormalizerStep)

    def test_registry_persists_across_registrations(self):
        """Test that registry maintains all registered steps."""

        @register_step("Step1")
        class Step1(BaseNormalizerStep):
            pass

        assert "Step1" in _STEP_REGISTRY

        @register_step("Step2")
        class Step2(BaseNormalizerStep):
            pass

        assert "Step1" in _STEP_REGISTRY
        assert "Step2" in _STEP_REGISTRY
        assert len(_STEP_REGISTRY) >= 2


class TestRegisterStepDecorator:
    """Test the register_step decorator functionality."""

    def setup_method(self):
        """Save the original registry state before each test."""
        self.original_registry = _STEP_REGISTRY.copy()

    def teardown_method(self):
        """Restore the original registry state after each test."""
        _STEP_REGISTRY.clear()
        _STEP_REGISTRY.update(self.original_registry)

    def test_register_step_basic(self):
        """Test basic registration of a step class."""

        @register_step("BasicStep")
        class BasicStep(BaseNormalizerStep):
            pass

        assert "BasicStep" in _STEP_REGISTRY
        assert _STEP_REGISTRY["BasicStep"] is BasicStep

    def test_register_step_with_custom_name(self):
        """Test registration with a custom name different from class name."""

        @register_step("CustomName")
        class MyStepClass(BaseNormalizerStep):
            pass

        assert "CustomName" in _STEP_REGISTRY
        assert _STEP_REGISTRY["CustomName"] is MyStepClass
        assert "MyStepClass" not in _STEP_REGISTRY

    def test_register_step_returns_class(self):
        """Test that decorator returns the class itself."""

        @register_step("ReturnTest")
        class ReturnTestStep(BaseNormalizerStep):
            pass

        # The decorator should return the original class
        assert ReturnTestStep.__name__ == "ReturnTestStep"
        report = NormalizationReport()
        instance = ReturnTestStep(config={}, report=report)
        assert isinstance(instance, BaseNormalizerStep)

    def test_register_step_duplicate_name_raises_error(self):
        """Test that registering duplicate name raises ValueError."""

        @register_step("DuplicateStep")
        class FirstStep(BaseNormalizerStep):
            pass

        with pytest.raises(ValueError, match="already registered"):

            @register_step("DuplicateStep")
            class SecondStep(BaseNormalizerStep):
                pass

    def test_register_step_with_empty_string_name(self):
        """Test registration with empty string name."""

        @register_step("")
        class EmptyNameStep(BaseNormalizerStep):
            pass

        assert "" in _STEP_REGISTRY

    def test_register_step_with_special_characters(self):
        """Test registration with special characters in name."""

        @register_step("Step-With-Dashes")
        class StepWithDashes(BaseNormalizerStep):
            pass

        assert "Step-With-Dashes" in _STEP_REGISTRY

        @register_step("Step.With.Dots")
        class StepWithDots(BaseNormalizerStep):
            pass

        assert "Step.With.Dots" in _STEP_REGISTRY

    def test_register_step_preserves_class_methods(self):
        """Test that registration preserves custom methods."""

        @register_step("MethodPreserveTest")
        class MethodPreserveTestStep(BaseNormalizerStep):
            def custom_method(self):
                return "custom"

            def process_lines(self, lines: Iterator[str]) -> Iterator[str]:
                yield from ["modified"]

        step_class = _STEP_REGISTRY["MethodPreserveTest"]
        report = NormalizationReport()
        instance = step_class(config={}, report=report)

        assert hasattr(instance, "custom_method")
        assert instance.custom_method() == "custom"
        assert list(instance.process_lines(iter([]))) == ["modified"]

    def test_register_step_preserves_class_attributes(self):
        """Test that registration preserves class attributes."""

        @register_step("AttributePreserveTest")
        class AttributePreserveTestStep(BaseNormalizerStep):
            class_attribute = "test_value"

        step_class = _STEP_REGISTRY["AttributePreserveTest"]
        assert step_class.class_attribute == "test_value"

    def test_register_multiple_steps_independently(self):
        """Test that multiple steps can be registered independently."""

        @register_step("IndependentStep1")
        class IndependentStep1(BaseNormalizerStep):
            pass

        @register_step("IndependentStep2")
        class IndependentStep2(BaseNormalizerStep):
            pass

        @register_step("IndependentStep3")
        class IndependentStep3(BaseNormalizerStep):
            pass

        assert (
            len([k for k in _STEP_REGISTRY.keys() if k.startswith("IndependentStep")])
            == 3
        )
        assert _STEP_REGISTRY["IndependentStep1"] is IndependentStep1
        assert _STEP_REGISTRY["IndependentStep2"] is IndependentStep2
        assert _STEP_REGISTRY["IndependentStep3"] is IndependentStep3

    def test_register_step_with_inheritance(self):
        """Test that subclasses can be registered."""

        @register_step("ParentStep")
        class ParentStep(BaseNormalizerStep):
            pass

        @register_step("ChildStep")
        class ChildStep(ParentStep):
            pass

        assert "ParentStep" in _STEP_REGISTRY
        assert "ChildStep" in _STEP_REGISTRY
        assert issubclass(_STEP_REGISTRY["ChildStep"], ParentStep)


class TestStepFactory:
    """Test the StepFactory class functionality."""

    def setup_method(self):
        """Save the original registry state and register test steps."""
        self.original_registry = _STEP_REGISTRY.copy()

        # Register test steps for factory tests
        @register_step("TestFactoryStep")
        class TestFactoryStep(BaseNormalizerStep):
            pass

        @register_step("ConfigurableStep")
        class ConfigurableStep(BaseNormalizerStep):
            def __init__(self, config, report, strict=False):
                super().__init__(config, report, strict)
                self.initialized = True

        self.TestFactoryStep = TestFactoryStep
        self.ConfigurableStep = ConfigurableStep

    def teardown_method(self):
        """Restore the original registry state after each test."""
        _STEP_REGISTRY.clear()
        _STEP_REGISTRY.update(self.original_registry)

    def test_factory_create_step_basic(self):
        """Test basic step creation through factory."""
        report = NormalizationReport()
        step_config = NormalizationStepConfig(
            step="TestFactoryStep", config={}, enabled=True
        )

        step = StepFactory.create_step(step_config, report)

        assert isinstance(step, BaseNormalizerStep)
        assert isinstance(step, self.TestFactoryStep)

    def test_factory_passes_config_correctly(self):
        """Test that factory passes configuration to step."""
        report = NormalizationReport()
        config_data = {"option1": "value1", "option2": 42}
        step_config = NormalizationStepConfig(
            step="ConfigurableStep", config=config_data, enabled=True
        )

        step = StepFactory.create_step(step_config, report)

        assert step.config == config_data
        assert step.config["option1"] == "value1"
        assert step.config["option2"] == 42

    def test_factory_passes_report_correctly(self):
        """Test that factory passes report to step."""
        report = NormalizationReport()
        report.add_warning("Test warning")
        step_config = NormalizationStepConfig(
            step="TestFactoryStep", config={}, enabled=True
        )

        step = StepFactory.create_step(step_config, report)

        assert step.report is report
        assert "Test warning" in step.report.warnings

    def test_factory_passes_strict_mode(self):
        """Test that factory passes strict mode to step."""
        report = NormalizationReport()
        step_config = NormalizationStepConfig(
            step="TestFactoryStep", config={}, enabled=True
        )

        # Non-strict mode
        step_lenient = StepFactory.create_step(step_config, report, strict=False)
        assert step_lenient.strict is False

        # Strict mode
        step_strict = StepFactory.create_step(step_config, report, strict=True)
        assert step_strict.strict is True

    def test_factory_unknown_step_raises_error(self):
        """Test that unknown step name raises ValueError."""
        report = NormalizationReport()
        step_config = NormalizationStepConfig(
            step="UnknownStepName", config={}, enabled=True
        )

        with pytest.raises(ValueError, match="not registered"):
            StepFactory.create_step(step_config, report)

    def test_factory_error_message_includes_step_name(self):
        """Test that error message includes the unknown step name."""
        report = NormalizationReport()
        step_config = NormalizationStepConfig(
            step="NonExistentStep", config={}, enabled=True
        )

        with pytest.raises(ValueError, match="NonExistentStep"):
            StepFactory.create_step(step_config, report)

    def test_factory_error_message_lists_available_steps(self):
        """Test that error message lists available steps."""
        report = NormalizationReport()
        step_config = NormalizationStepConfig(
            step="InvalidStep", config={}, enabled=True
        )

        with pytest.raises(ValueError, match="Available steps"):
            StepFactory.create_step(step_config, report)

    def test_factory_creates_independent_instances(self):
        """Test that factory creates independent instances."""
        report = NormalizationReport()
        step_config = NormalizationStepConfig(
            step="ConfigurableStep", config={}, enabled=True
        )

        step1 = StepFactory.create_step(step_config, report)
        step2 = StepFactory.create_step(step_config, report)

        assert step1 is not step2
        assert isinstance(step1, self.ConfigurableStep)
        assert isinstance(step2, self.ConfigurableStep)

    def test_factory_with_complex_config(self):
        """Test factory with complex nested configuration."""
        report = NormalizationReport()
        complex_config = {
            "nested": {"level1": {"level2": ["value1", "value2"]}},
            "patterns": ["*.csv", "*.txt"],
            "enabled": True,
        }
        step_config = NormalizationStepConfig(
            step="ConfigurableStep", config=complex_config, enabled=True
        )

        step = StepFactory.create_step(step_config, report)

        assert step.config == complex_config
        assert step.config["nested"]["level1"]["level2"] == ["value1", "value2"]

    def test_factory_with_empty_config(self):
        """Test factory with empty configuration."""
        report = NormalizationReport()
        step_config = NormalizationStepConfig(
            step="TestFactoryStep", config={}, enabled=True
        )

        step = StepFactory.create_step(step_config, report)

        assert step.config == {}

    def test_factory_static_method(self):
        """Test that create_step is a static method."""
        assert isinstance(StepFactory.__dict__["create_step"], staticmethod)

    def test_factory_multiple_steps_creation(self):
        """Test creating multiple different steps."""
        report = NormalizationReport()

        config1 = NormalizationStepConfig(step="TestFactoryStep", config={})
        config2 = NormalizationStepConfig(step="ConfigurableStep", config={})

        step1 = StepFactory.create_step(config1, report)
        step2 = StepFactory.create_step(config2, report)

        assert isinstance(step1, self.TestFactoryStep)
        assert isinstance(step2, self.ConfigurableStep)
        assert type(step1) is not type(step2)


class TestStepRegistryAndFactoryIntegration:
    """Test integration between registry, decorator, and factory."""

    def setup_method(self):
        """Save the original registry state."""
        self.original_registry = _STEP_REGISTRY.copy()

    def teardown_method(self):
        """Restore the original registry state."""
        _STEP_REGISTRY.clear()
        _STEP_REGISTRY.update(self.original_registry)

    def test_full_workflow_register_and_create(self):
        """Test complete workflow: register step and create via factory."""

        @register_step("WorkflowStep")
        class WorkflowStep(BaseNormalizerStep):
            def process_lines(self, lines: Iterator[str]) -> Iterator[str]:
                for line in lines:
                    yield line.upper()

        report = NormalizationReport()
        step_config = NormalizationStepConfig(
            step="WorkflowStep", config={}, enabled=True
        )

        step = StepFactory.create_step(step_config, report)

        # Test that created step works as expected
        result = list(step.process_lines(iter(["hello\n", "world\n"])))
        assert result == ["HELLO\n", "WORLD\n"]

    def test_multiple_steps_workflow(self):
        """Test registering and creating multiple steps in sequence."""

        @register_step("Step1")
        class Step1(BaseNormalizerStep):
            pass

        @register_step("Step2")
        class Step2(BaseNormalizerStep):
            pass

        @register_step("Step3")
        class Step3(BaseNormalizerStep):
            pass

        report = NormalizationReport()

        # Create instances of all steps
        steps = []
        for step_name in ["Step1", "Step2", "Step3"]:
            config = NormalizationStepConfig(step=step_name, config={})
            steps.append(StepFactory.create_step(config, report))

        assert len(steps) == 3
        assert all(isinstance(s, BaseNormalizerStep) for s in steps)

    def test_registry_isolation_between_tests(self):
        """Test that registry cleanup works between tests."""

        # Register a step
        @register_step("IsolationTestStep")
        class IsolationTestStep(BaseNormalizerStep):
            pass

        assert "IsolationTestStep" in _STEP_REGISTRY

    def test_factory_with_shared_report_multiple_steps(self):
        """Test that multiple steps can share the same report."""

        @register_step("SharedReportStep1")
        class SharedReportStep1(BaseNormalizerStep):
            pass

        @register_step("SharedReportStep2")
        class SharedReportStep2(BaseNormalizerStep):
            pass

        report = NormalizationReport()

        config1 = NormalizationStepConfig(step="SharedReportStep1", config={})
        config2 = NormalizationStepConfig(step="SharedReportStep2", config={})

        step1 = StepFactory.create_step(config1, report)
        step2 = StepFactory.create_step(config2, report)

        # Both steps share the same report
        assert step1.report is report
        assert step2.report is report
        assert step1.report is step2.report

        # Changes to report are visible to both
        step1.report.add_warning("Warning from step1")
        assert "Warning from step1" in step2.report.warnings

    def test_step_config_enabled_flag_not_used_by_factory(self):
        """Test that factory doesn't check the enabled flag."""

        @register_step("DisabledStep")
        class DisabledStep(BaseNormalizerStep):
            pass

        report = NormalizationReport()
        step_config = NormalizationStepConfig(
            step="DisabledStep",
            config={},
            enabled=False,  # Disabled, but factory should still create it
        )

        # Factory should create the step regardless of enabled flag
        step = StepFactory.create_step(step_config, report)
        assert isinstance(step, DisabledStep)
