"""Tests for post-load quality checks."""

from __future__ import annotations

import pytest

from pycharter.etl_generator.quality import (
    QualityChecker,
    QualityCheckResult,
    QualityCheckSeverity,
    QualityCheckStatus,
    QualityReport,
    check_expression,
    check_null_rate,
    check_row_count,
    check_uniqueness,
    create_quality_checker,
)
from pycharter.etl_generator.result import LoadResult


def _make_load_result(rows_loaded: int = 10, rows_failed: int = 0) -> LoadResult:
    """Create a LoadResult for testing."""
    return LoadResult(success=True, rows_loaded=rows_loaded, rows_failed=rows_failed)


# =============================================================================
# check_row_count
# =============================================================================


class TestCheckRowCount:
    """Tests for check_row_count."""

    def test_min_passes(self):
        """Count above minimum passes."""
        records = [{"id": i} for i in range(10)]
        result = check_row_count(records, _make_load_result(), {"min": 5})
        assert result.status == QualityCheckStatus.PASSED

    def test_min_fails(self):
        """Count below minimum fails."""
        records = [{"id": 1}]
        result = check_row_count(records, _make_load_result(), {"min": 5})
        assert result.status == QualityCheckStatus.FAILED
        assert "< minimum 5" in result.message

    def test_max_passes(self):
        """Count below maximum passes."""
        records = [{"id": i} for i in range(5)]
        result = check_row_count(records, _make_load_result(), {"max": 10})
        assert result.status == QualityCheckStatus.PASSED

    def test_max_fails(self):
        """Count above maximum fails."""
        records = [{"id": i} for i in range(20)]
        result = check_row_count(records, _make_load_result(), {"max": 10})
        assert result.status == QualityCheckStatus.FAILED
        assert "> maximum 10" in result.message

    def test_exact_passes(self):
        """Exact count match passes."""
        records = [{"id": i} for i in range(5)]
        result = check_row_count(records, _make_load_result(), {"exact": 5})
        assert result.status == QualityCheckStatus.PASSED

    def test_exact_fails(self):
        """Exact count mismatch fails."""
        records = [{"id": i} for i in range(3)]
        result = check_row_count(records, _make_load_result(), {"exact": 5})
        assert result.status == QualityCheckStatus.FAILED
        assert "!= expected 5" in result.message

    def test_expected_within_tolerance(self):
        """Count within tolerance range passes."""
        records = [{"id": i} for i in range(95)]
        result = check_row_count(
            records,
            _make_load_result(),
            {"expected": 100, "tolerance_percent": 10},
        )
        assert result.status == QualityCheckStatus.PASSED

    def test_expected_outside_tolerance(self):
        """Count outside tolerance range fails."""
        records = [{"id": i} for i in range(80)]
        result = check_row_count(
            records,
            _make_load_result(),
            {"expected": 100, "tolerance_percent": 10},
        )
        assert result.status == QualityCheckStatus.FAILED
        assert "outside expected range" in result.message

    def test_no_constraints(self):
        """No constraints always passes."""
        records = [{"id": 1}]
        result = check_row_count(records, _make_load_result(), {})
        assert result.status == QualityCheckStatus.PASSED


# =============================================================================
# check_null_rate
# =============================================================================


class TestCheckNullRate:
    """Tests for check_null_rate."""

    def test_no_nulls(self):
        """No nulls passes."""
        records = [{"id": 1}, {"id": 2}]
        result = check_null_rate(records, _make_load_result(), {"fields": ["id"]})
        assert result.status == QualityCheckStatus.PASSED

    def test_with_nulls_fails(self):
        """Nulls above threshold fails."""
        records = [{"id": 1}, {"id": None}]
        result = check_null_rate(
            records, _make_load_result(), {"fields": ["id"], "max_null_percent": 0}
        )
        assert result.status == QualityCheckStatus.FAILED

    def test_within_tolerance(self):
        """Null rate within tolerance passes."""
        records = [{"id": 1}, {"id": None}, {"id": 3}]
        result = check_null_rate(
            records,
            _make_load_result(),
            {"fields": ["id"], "max_null_percent": 50},
        )
        assert result.status == QualityCheckStatus.PASSED

    def test_missing_field_as_null(self):
        """Missing field treated as null."""
        records = [{"name": "Alice"}, {"name": "Bob"}]
        result = check_null_rate(
            records, _make_load_result(), {"fields": ["id"], "max_null_percent": 0}
        )
        assert result.status == QualityCheckStatus.FAILED

    def test_empty_records(self):
        """Empty records passes."""
        result = check_null_rate([], _make_load_result(0), {"fields": ["id"]})
        assert result.status == QualityCheckStatus.PASSED


# =============================================================================
# check_uniqueness
# =============================================================================


class TestCheckUniqueness:
    """Tests for check_uniqueness."""

    def test_all_unique(self):
        """All unique values pass."""
        records = [{"id": 1}, {"id": 2}, {"id": 3}]
        result = check_uniqueness(records, _make_load_result(), {"fields": ["id"]})
        assert result.status == QualityCheckStatus.PASSED

    def test_duplicates_fail(self):
        """Duplicate values fail."""
        records = [{"id": 1}, {"id": 1}, {"id": 2}]
        result = check_uniqueness(records, _make_load_result(), {"fields": ["id"]})
        assert result.status == QualityCheckStatus.FAILED
        assert "duplicate" in result.message.lower()

    def test_null_values_ignored(self):
        """Null values are excluded from uniqueness check."""
        records = [{"id": None}, {"id": None}, {"id": 1}]
        result = check_uniqueness(records, _make_load_result(), {"fields": ["id"]})
        assert result.status == QualityCheckStatus.PASSED


# =============================================================================
# check_expression
# =============================================================================


class TestCheckExpression:
    """Tests for check_expression."""

    def test_true_passes(self):
        """Truthy expression passes."""
        records = [{"id": i} for i in range(5)]
        result = check_expression(
            records,
            _make_load_result(5),
            {"expression": "row_count == 5", "description": "Five rows"},
        )
        assert result.status == QualityCheckStatus.PASSED

    def test_false_fails(self):
        """Falsy expression fails."""
        records = [{"id": 1}]
        result = check_expression(
            records,
            _make_load_result(1),
            {"expression": "row_count > 10", "description": "Enough rows"},
        )
        assert result.status == QualityCheckStatus.FAILED

    def test_invalid_expression_skips(self):
        """Invalid expression is skipped."""
        result = check_expression(
            [{"id": 1}],
            _make_load_result(),
            {"expression": "invalid_var + !", "description": "Bad expr"},
        )
        assert result.status == QualityCheckStatus.SKIPPED

    def test_restricted_builtins(self):
        """Only safe builtins are available."""
        records = [{"id": 1}, {"id": 2}]
        result = check_expression(
            records,
            _make_load_result(2),
            {"expression": "len(records) == 2", "description": "len works"},
        )
        assert result.status == QualityCheckStatus.PASSED

    def test_empty_expression_skips(self):
        """Empty expression is skipped."""
        result = check_expression(
            [{"id": 1}],
            _make_load_result(),
            {"expression": "", "description": "Empty"},
        )
        assert result.status == QualityCheckStatus.SKIPPED


# =============================================================================
# QualityChecker
# =============================================================================


class TestQualityChecker:
    """Tests for QualityChecker."""

    def test_all_pass(self):
        """All checks passing produces a passed report."""
        checker = QualityChecker(
            [{"type": "row_count", "min": 1}], pipeline_name="test"
        )
        records = [{"id": 1}]
        report = checker.run(records, _make_load_result(1))

        assert report.passed is True
        assert len(report.checks) == 1

    def test_mixed_results(self):
        """Mixed pass/fail results."""
        checker = QualityChecker(
            [
                {"type": "row_count", "min": 100, "severity": "fail"},
                {"type": "row_count", "max": 1000, "severity": "warn"},
            ],
            pipeline_name="test",
        )
        records = [{"id": i} for i in range(5)]
        report = checker.run(records, _make_load_result(5))

        assert report.passed is False
        assert len(report.failures) == 1

    def test_fail_severity(self):
        """FAIL severity causes report.passed = False."""
        checker = QualityChecker(
            [{"type": "row_count", "exact": 99, "severity": "fail"}]
        )
        records = [{"id": 1}]
        report = checker.run(records, _make_load_result(1))

        assert report.passed is False
        assert len(report.hard_failures) == 1

    def test_warn_severity(self):
        """WARN severity keeps report.passed = True."""
        checker = QualityChecker(
            [{"type": "row_count", "exact": 99, "severity": "warn"}]
        )
        records = [{"id": 1}]
        report = checker.run(records, _make_load_result(1))

        assert report.passed is True
        assert len(report.warnings) == 1

    def test_empty_checks(self):
        """Empty checks list produces passed report."""
        checker = QualityChecker([])
        report = checker.run([{"id": 1}], _make_load_result(1))

        assert report.passed is True
        assert len(report.checks) == 0

    def test_unknown_type_skipped(self):
        """Unknown check type is skipped."""
        checker = QualityChecker([{"type": "nonexistent_check"}])
        report = checker.run([{"id": 1}], _make_load_result(1))

        assert report.passed is True
        assert report.checks[0].status == QualityCheckStatus.SKIPPED


# =============================================================================
# QualityReport
# =============================================================================


class TestQualityReport:
    """Tests for QualityReport."""

    def test_to_dict(self):
        """to_dict includes all expected fields."""
        check = QualityCheckResult(
            check_name="test",
            check_type="row_count",
            status=QualityCheckStatus.PASSED,
            severity=QualityCheckSeverity.FAIL,
            message="OK",
        )
        report = QualityReport(checks=(check,), passed=True)
        d = report.to_dict()

        assert d["passed"] is True
        assert d["total_checks"] == 1
        assert d["failed_count"] == 0
        assert len(d["checks"]) == 1

    def test_properties(self):
        """Properties filter checks correctly."""
        passed = QualityCheckResult(
            check_name="ok",
            check_type="row_count",
            status=QualityCheckStatus.PASSED,
            severity=QualityCheckSeverity.FAIL,
            message="OK",
        )
        failed_hard = QualityCheckResult(
            check_name="bad",
            check_type="null_rate",
            status=QualityCheckStatus.FAILED,
            severity=QualityCheckSeverity.FAIL,
            message="Bad",
        )
        failed_warn = QualityCheckResult(
            check_name="meh",
            check_type="uniqueness",
            status=QualityCheckStatus.FAILED,
            severity=QualityCheckSeverity.WARN,
            message="Meh",
        )
        report = QualityReport(checks=(passed, failed_hard, failed_warn), passed=False)

        assert len(report.failures) == 2
        assert len(report.hard_failures) == 1
        assert len(report.warnings) == 1


# =============================================================================
# Factory
# =============================================================================


class TestQualityCheckerFactory:
    """Tests for create_quality_checker."""

    def test_from_config(self):
        """Creates checker from config list."""
        checker = create_quality_checker(
            [{"type": "row_count", "min": 1}], pipeline_name="test"
        )
        assert checker is not None

    def test_from_none(self):
        """Returns None from None config."""
        checker = create_quality_checker(None)
        assert checker is None

    def test_from_empty(self):
        """Returns None from empty list."""
        checker = create_quality_checker([])
        assert checker is None


# =============================================================================
# Pipeline Integration
# =============================================================================


class TestPipelineQualityIntegration:
    """Tests for quality checks integrated with Pipeline."""

    @pytest.mark.asyncio
    async def test_checks_in_result(self):
        """Quality report appears in PipelineResult."""
        from pycharter.etl_generator.pipeline import Pipeline
        from pycharter.etl_generator.testing import MockExtractor, MockLoader

        pipeline = Pipeline(
            name="test",
            quality_checks_config=[{"type": "row_count", "min": 1}],
        )
        pipeline.extractor = MockExtractor([{"id": 1}])
        pipeline.loader = MockLoader()

        result = await pipeline.run()

        assert result.success is True
        assert result.quality_report is not None
        assert result.quality_report.passed is True

    @pytest.mark.asyncio
    async def test_fail_sets_pipeline_failure(self):
        """FAIL severity check failure sets pipeline success to False."""
        from pycharter.etl_generator.pipeline import Pipeline
        from pycharter.etl_generator.testing import MockExtractor, MockLoader

        pipeline = Pipeline(
            name="test",
            quality_checks_config=[
                {"type": "row_count", "exact": 99, "severity": "fail"}
            ],
        )
        pipeline.extractor = MockExtractor([{"id": 1}])
        pipeline.loader = MockLoader()

        result = await pipeline.run()

        assert result.success is False
        assert any("Quality check failed" in e for e in result.errors)

    @pytest.mark.asyncio
    async def test_warn_keeps_success(self):
        """WARN severity check failure keeps pipeline success True."""
        from pycharter.etl_generator.pipeline import Pipeline
        from pycharter.etl_generator.testing import MockExtractor, MockLoader

        pipeline = Pipeline(
            name="test",
            quality_checks_config=[
                {"type": "row_count", "exact": 99, "severity": "warn"}
            ],
        )
        pipeline.extractor = MockExtractor([{"id": 1}])
        pipeline.loader = MockLoader()

        result = await pipeline.run()

        assert result.success is True
        assert result.quality_report is not None
        assert len(result.quality_report.warnings) == 1

    @pytest.mark.asyncio
    async def test_config_from_yaml(self, tmp_path):
        """Quality checks work when loaded from YAML config."""
        import yaml

        load_config = {
            "type": "file",
            "path": str(tmp_path / "output.json"),
            "quality_checks": [
                {"type": "row_count", "min": 1},
            ],
        }
        extract_config = {
            "type": "file",
            "path": str(tmp_path / "input.json"),
        }

        # Write input file
        input_file = tmp_path / "input.json"
        input_file.write_text('[{"id": 1}]')

        extract_file = tmp_path / "extract.yaml"
        extract_file.write_text(yaml.dump(extract_config))
        load_file = tmp_path / "load.yaml"
        load_file.write_text(yaml.dump(load_config))

        from pycharter.etl_generator.pipeline import Pipeline

        pipeline = Pipeline.from_config_files(
            extract=extract_file,
            load=load_file,
            validate=False,
        )

        # Verify quality_checks_config was extracted
        assert pipeline._quality_checks_config == [{"type": "row_count", "min": 1}]
