"""
Comprehensive test suite for steps.py module.

Tests cover:
- BaseNormalizerStep class functionality
- Step registry and registration decorator
- StepFactory class and step creation
- Error handling and validation
- Integration with NormalizationReport
"""

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


class TestBaseNormalizerStep:
    """Test the BaseNormalizerStep base class."""

    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

    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"])
        result = list(step.process_lines(lines))

        assert result == ["single line"]

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

        input_lines = ["line 1", "line 2", "line 3", "line 4"]
        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(["  leading", "trailing  ", "  both  ", "\ttabs\t"])
        result = list(step.process_lines(lines))

        assert result == ["  leading", "trailing  ", "  both  ", "\ttabs\t"]

    def test_process_lines_preserves_empty_lines(self):
        """Test that empty lines are preserved in default implementation."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        lines = iter(["line 1", "", "line 3", "", ""])
        result = list(step.process_lines(lines))

        assert result == ["line 1", "", "line 3", "", ""]

    def test_process_lines_yields_lazily(self):
        """Test that process_lines yields items lazily without consuming the entire iterator."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        def lazy_generator():
            yield "item 1"
            yield "item 2"
            yield "item 3"

        result_iterator = step.process_lines(lazy_generator())

        # Should be an iterator, not a list
        assert hasattr(result_iterator, "__iter__")
        assert hasattr(result_iterator, "__next__")

        # Can consume one item at a time
        assert next(result_iterator) == "item 1"
        assert next(result_iterator) == "item 2"
        assert next(result_iterator) == "item 3"

    def test_process_row_passthrough_none(self):
        """Test that None row is handled correctly."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        result = step.process_row(None)

        assert result is None

    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_simple_dict(self):
        """Test that simple dictionary passes through unchanged."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

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

        assert result == row

    def test_process_row_returns_same_reference(self):
        """Test that process_row returns the same dictionary reference."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

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

        assert result is row

    def test_process_row_preserves_complex_values(self):
        """Test that complex values in rows are preserved."""
        report = NormalizationReport()
        step = BaseNormalizerStep(config={}, report=report)

        row = {
            "id": 1,
            "nested": {"key": "value"},
            "list": [1, 2, 3],
            "null": None,
            "bool": True,
        }
        result = step.process_row(row)

        assert result == row
        assert result["nested"] == {"key": "value"}
        assert result["list"] == [1, 2, 3]


class TestStepRegistry:
    """Test the step registry and registration decorator."""

    def test_register_step_decorator_basic(self):
        """Test basic step registration."""
        # Clear registry for this test
        initial_registry = _STEP_REGISTRY.copy()

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

        assert "TestStep" in _STEP_REGISTRY
        assert _STEP_REGISTRY["TestStep"] is TestStep

        # Clean up
        _STEP_REGISTRY.clear()
        _STEP_REGISTRY.update(initial_registry)

    def test_register_step_decorator_multiple_steps(self):
        """Test registering multiple different steps."""
        initial_registry = _STEP_REGISTRY.copy()

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

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

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

        assert "Step1" in _STEP_REGISTRY
        assert "Step2" in _STEP_REGISTRY
        assert "Step3" in _STEP_REGISTRY
        assert _STEP_REGISTRY["Step1"] is Step1
        assert _STEP_REGISTRY["Step2"] is Step2
        assert _STEP_REGISTRY["Step3"] is Step3

        # Clean up
        _STEP_REGISTRY.clear()
        _STEP_REGISTRY.update(initial_registry)

    def test_register_step_duplicate_name_raises_error(self):
        """Test that registering a step with duplicate name raises ValueError."""
        initial_registry = _STEP_REGISTRY.copy()

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

        with pytest.raises(
            ValueError, match="Step 'DuplicateStep' is already registered"
        ):

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

        # Clean up
        _STEP_REGISTRY.clear()
        _STEP_REGISTRY.update(initial_registry)

    def test_register_step_returns_class(self):
        """Test that register_step decorator returns the class unchanged."""
        initial_registry = _STEP_REGISTRY.copy()

        @register_step("ReturnedStep")
        class OriginalStep(BaseNormalizerStep):
            pass

        # Should be able to instantiate the class
        report = NormalizationReport()
        instance = OriginalStep(config={}, report=report)
        assert isinstance(instance, OriginalStep)
        assert isinstance(instance, BaseNormalizerStep)

        # Clean up
        _STEP_REGISTRY.clear()
        _STEP_REGISTRY.update(initial_registry)

    def test_register_step_with_custom_methods(self):
        """Test registering a step with custom process methods."""
        initial_registry = _STEP_REGISTRY.copy()

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

            def process_row(self, row: Dict[str, Any]) -> Optional[Dict[str, Any]]:
                return {k: str(v).upper() for k, v in row.items()}

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

        # Test custom process_lines
        lines = list(step.process_lines(iter(["hello", "world"])))
        assert lines == ["HELLO", "WORLD"]

        # Test custom process_row
        row = step.process_row({"name": "alice", "role": "admin"})
        assert row == {"name": "ALICE", "role": "ADMIN"}

        # Clean up
        _STEP_REGISTRY.clear()
        _STEP_REGISTRY.update(initial_registry)


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

    def test_create_step_with_registered_step(self):
        """Test creating a step from a registered step class."""
        initial_registry = _STEP_REGISTRY.copy()

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

        report = NormalizationReport()
        step_config = NormalizationStepConfig(
            step="FactoryTestStep",
            config={"key": "value"},
            enabled=True,
        )

        step = StepFactory.create_step(step_config, report)

        assert isinstance(step, FactoryTestStep)
        assert isinstance(step, BaseNormalizerStep)
        assert step.config == {"key": "value"}
        assert step.report is report
        assert step.strict is False

        # Clean up
        _STEP_REGISTRY.clear()
        _STEP_REGISTRY.update(initial_registry)

    def test_create_step_with_strict_mode(self):
        """Test creating a step with strict mode enabled."""
        initial_registry = _STEP_REGISTRY.copy()

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

        report = NormalizationReport()
        step_config = NormalizationStepConfig(
            step="StrictTestStep",
            config={},
        )

        step = StepFactory.create_step(step_config, report, strict=True)

        assert step.strict is True

        # Clean up
        _STEP_REGISTRY.clear()
        _STEP_REGISTRY.update(initial_registry)

    def test_create_step_with_empty_config(self):
        """Test creating a step with empty config."""
        initial_registry = _STEP_REGISTRY.copy()

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

        report = NormalizationReport()
        step_config = NormalizationStepConfig(step="EmptyConfigStep")

        step = StepFactory.create_step(step_config, report)

        assert step.config == {}

        # Clean up
        _STEP_REGISTRY.clear()
        _STEP_REGISTRY.update(initial_registry)

    def test_create_step_with_complex_config(self):
        """Test creating a step with complex configuration."""
        initial_registry = _STEP_REGISTRY.copy()

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

        report = NormalizationReport()
        complex_config = {
            "patterns": ["*.csv", "*.txt"],
            "options": {
                "nested": {"value": 42},
                "list": [1, 2, 3],
            },
            "enabled": True,
        }
        step_config = NormalizationStepConfig(
            step="ComplexConfigStep",
            config=complex_config,
        )

        step = StepFactory.create_step(step_config, report)

        assert step.config == complex_config
        assert step.config["options"]["nested"]["value"] == 42

        # Clean up
        _STEP_REGISTRY.clear()
        _STEP_REGISTRY.update(initial_registry)

    def test_create_step_unregistered_step_raises_error(self):
        """Test that creating an unregistered step raises ValueError."""
        report = NormalizationReport()
        step_config = NormalizationStepConfig(
            step="NonExistentStep",
            config={},
        )

        with pytest.raises(ValueError) as exc_info:
            StepFactory.create_step(step_config, report)

        error_message = str(exc_info.value)
        assert "Normalization step 'NonExistentStep' is not registered" in error_message
        assert "Available steps:" in error_message

    def test_create_step_error_message_lists_available_steps(self):
        """Test that error message lists all available registered steps."""
        initial_registry = _STEP_REGISTRY.copy()

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

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

        report = NormalizationReport()
        step_config = NormalizationStepConfig(step="InvalidStep")

        with pytest.raises(ValueError) as exc_info:
            StepFactory.create_step(step_config, report)

        error_message = str(exc_info.value)
        assert "AvailableStep1" in error_message
        assert "AvailableStep2" in error_message

        # Clean up
        _STEP_REGISTRY.clear()
        _STEP_REGISTRY.update(initial_registry)

    def test_create_step_shares_report_reference(self):
        """Test that created step shares the same report reference."""
        initial_registry = _STEP_REGISTRY.copy()

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

        report = NormalizationReport()
        step_config = NormalizationStepConfig(step="SharedReportStep")

        step = StepFactory.create_step(step_config, report)

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

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

        # Clean up
        _STEP_REGISTRY.clear()
        _STEP_REGISTRY.update(initial_registry)


class TestCustomStepImplementations:
    """Test custom implementations of BaseNormalizerStep."""

    def test_custom_step_overrides_process_lines(self):
        """Test a custom step that overrides process_lines."""

        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)

        input_lines = ["hello", "world", "test"]
        result = list(step.process_lines(iter(input_lines)))

        assert result == ["HELLO", "WORLD", "TEST"]

    def test_custom_step_overrides_process_row(self):
        """Test a custom step that overrides process_row."""

        class MultiplyIdStep(BaseNormalizerStep):
            def process_row(self, row: Dict[str, Any]) -> Optional[Dict[str, Any]]:
                if row and "id" in row:
                    row["id"] = row["id"] * 2
                return row

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

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

        assert result["id"] == 10
        assert result["name"] == "test"

    def test_custom_step_filters_rows(self):
        """Test a custom step that filters rows by returning None."""

        class FilterEvenIdStep(BaseNormalizerStep):
            def process_row(self, row: Dict[str, Any]) -> Optional[Dict[str, Any]]:
                if row and "id" in row and row["id"] % 2 == 0:
                    return None  # Filter out even IDs
                return row

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

        assert step.process_row({"id": 1}) == {"id": 1}
        assert step.process_row({"id": 2}) is None
        assert step.process_row({"id": 3}) == {"id": 3}
        assert step.process_row({"id": 4}) is None

    def test_custom_step_uses_config(self):
        """Test a custom step that uses configuration."""

        class ConfigurableFilterStep(BaseNormalizerStep):
            def process_row(self, row: Dict[str, Any]) -> Optional[Dict[str, Any]]:
                min_id = self.config.get("min_id", 0)
                if row and "id" in row and row["id"] < min_id:
                    return None
                return row

        report = NormalizationReport()
        step = ConfigurableFilterStep(config={"min_id": 5}, report=report)

        assert step.process_row({"id": 3}) is None
        assert step.process_row({"id": 5}) == {"id": 5}
        assert step.process_row({"id": 7}) == {"id": 7}

    def test_custom_step_uses_report(self):
        """Test a custom step that uses the report object."""

        class ReportingStep(BaseNormalizerStep):
            def process_row(self, row: Dict[str, Any]) -> Optional[Dict[str, Any]]:
                if row and row.get("status") == "error":
                    self.report.add_warning(
                        f"Error status found for id {row.get('id')}"
                    )
                return row

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

        step.process_row({"id": 1, "status": "ok"})
        assert len(report.warnings) == 0

        step.process_row({"id": 2, "status": "error"})
        assert len(report.warnings) == 1
        assert "Error status found for id 2" in report.warnings[0]

    def test_custom_step_with_strict_mode(self):
        """Test a custom step that behaves differently in strict mode."""

        class StrictAwareStep(BaseNormalizerStep):
            def process_row(self, row: Dict[str, Any]) -> Optional[Dict[str, Any]]:
                if row and "required_field" not in row:
                    if self.strict:
                        raise ValueError("required_field is missing")
                    else:
                        self.report.add_warning("required_field is missing")
                        return None
                return row

        report_strict = NormalizationReport()
        step_strict = StrictAwareStep(config={}, report=report_strict, strict=True)

        with pytest.raises(ValueError, match="required_field is missing"):
            step_strict.process_row({"id": 1})

        report_lenient = NormalizationReport()
        step_lenient = StrictAwareStep(config={}, report=report_lenient, strict=False)

        result = step_lenient.process_row({"id": 1})
        assert result is None
        assert len(report_lenient.warnings) == 1
