"""Tests for quality gate engine."""

from __future__ import annotations

import time

import pytest

from orxaq.fabric.quality import (
    BoundsGate,
    CompletenessGate,
    CustomGate,
    FreshnessGate,
    QualityError,
    QualityGate,
    QualityPipeline,
    TypeGate,
    UniquenessGate,
)
from orxaq.fabric.schema import ColumnDef, SchemaSpec
from orxaq.kernel import AuditLog


class TestCompletenessGate:
    def test_complete_data_passes(self):
        data = [{"a": 1, "b": 2}, {"a": 3, "b": 4}]
        gate = CompletenessGate(["a", "b"])
        result = gate.check(data)
        assert result.passed is True
        assert result.score == 1.0

    def test_incomplete_data_fails(self):
        data = [{"a": 1, "b": None}, {"a": None, "b": None}]
        gate = CompletenessGate(["a", "b"], threshold=0.95)
        result = gate.check(data)
        assert result.passed is False

    def test_threshold_boundary(self):
        # 9 out of 10 non-null = 90% — fails at 95% threshold
        data = [{"x": i if i < 9 else None} for i in range(10)]
        gate = CompletenessGate(["x"], threshold=0.95)
        assert gate.check(data).passed is False

        gate_relaxed = CompletenessGate(["x"], threshold=0.80)
        assert gate_relaxed.check(data).passed is True

    def test_empty_data(self):
        result = CompletenessGate(["a"]).check([])
        assert result.passed is True


class TestBoundsGate:
    def test_in_bounds_passes(self):
        data = [{"score": 50}, {"score": 80}]
        gate = BoundsGate("score", min_val=0, max_val=100)
        assert gate.check(data).passed is True

    def test_below_min_fails(self):
        data = [{"score": -5}]
        gate = BoundsGate("score", min_val=0)
        result = gate.check(data)
        assert result.passed is False
        assert len(result.failures) == 1

    def test_above_max_fails(self):
        data = [{"score": 150}]
        gate = BoundsGate("score", max_val=100)
        assert gate.check(data).passed is False

    def test_null_values_skipped(self):
        data = [{"score": None}, {"score": 50}]
        gate = BoundsGate("score", min_val=0, max_val=100)
        assert gate.check(data).passed is True


class TestTypeGate:
    def test_correct_types_pass(self):
        schema = SchemaSpec(name="t", columns=(ColumnDef("x", "float"),))
        data = [{"x": 1.0}, {"x": 2}]
        assert TypeGate(schema).check(data).passed is True

    def test_wrong_type_fails(self):
        schema = SchemaSpec(name="t", columns=(ColumnDef("x", "float"),))
        data = [{"x": "not_a_number"}]
        result = TypeGate(schema).check(data)
        assert result.passed is False


class TestUniquenessGate:
    def test_unique_passes(self):
        data = [{"id": 1}, {"id": 2}, {"id": 3}]
        assert UniquenessGate(["id"]).check(data).passed is True

    def test_duplicates_fail(self):
        data = [{"id": 1}, {"id": 1}, {"id": 2}]
        result = UniquenessGate(["id"]).check(data)
        assert result.passed is False
        assert len(result.failures) == 1

    def test_composite_key(self):
        data = [{"a": 1, "b": 1}, {"a": 1, "b": 2}, {"a": 1, "b": 1}]
        result = UniquenessGate(["a", "b"]).check(data)
        assert result.passed is False


class TestFreshnessGate:
    def test_fresh_data_passes(self):
        now = time.time()
        data = [{"ts": now - 60}]  # 1 minute ago
        gate = FreshnessGate("ts", max_age_hours=1)
        assert gate.check(data).passed is True

    def test_stale_data_fails(self):
        old = time.time() - 7200  # 2 hours ago
        data = [{"ts": old}]
        gate = FreshnessGate("ts", max_age_hours=1)
        assert gate.check(data).passed is False


class TestCustomGate:
    def test_passing_predicate(self):
        gate = CustomGate("positive_check", lambda data: all(r.get("x", 0) > 0 for r in data))
        data = [{"x": 1}, {"x": 2}]
        assert gate.check(data).passed is True

    def test_failing_predicate(self):
        gate = CustomGate("positive_check", lambda data: all(r.get("x", 0) > 0 for r in data))
        data = [{"x": -1}]
        assert gate.check(data).passed is False


class TestQualityPipeline:
    def test_all_pass(self):
        gates = [
            CompletenessGate(["a"], threshold=0.5),
            BoundsGate("a", min_val=0, max_val=100),
        ]
        data = [{"a": 50}]
        result = QualityPipeline(gates).run(data)
        assert result.all_passed is True
        assert len(result.gate_results) == 2

    def test_partial_fail(self):
        gates = [
            CompletenessGate(["a"]),
            BoundsGate("a", max_val=10),
        ]
        data = [{"a": 50}]
        result = QualityPipeline(gates).run(data)
        assert result.all_passed is False

    def test_run_or_reject_passes(self):
        gates = [CompletenessGate(["a"])]
        data = [{"a": 1}]
        result = QualityPipeline(gates).run_or_reject(data)
        assert result == data

    def test_run_or_reject_raises(self):
        gates = [BoundsGate("a", max_val=0)]
        data = [{"a": 50}]
        with pytest.raises(QualityError, match="bounds:a"):
            QualityPipeline(gates).run_or_reject(data)

    def test_audit_logging(self):
        audit = AuditLog()
        gates = [CompletenessGate(["a"])]
        QualityPipeline(gates, audit_log=audit).run([{"a": 1}])
        entries = audit.search(action="quality_gate:completeness")
        assert len(entries) == 1
        assert "PASS" in entries[0].details

    def test_overall_score(self):
        gates = [
            CompletenessGate(["a"]),
            BoundsGate("a", min_val=0, max_val=100),
        ]
        data = [{"a": 50}]
        result = QualityPipeline(gates).run(data)
        assert result.overall_score == 1.0


class TestQualityGateBase:
    def test_base_check_raises(self):
        with pytest.raises(NotImplementedError):
            QualityGate().check([{"a": 1}])


class TestBoundsGateEdgeCases:
    def test_empty_data_passes(self):
        gate = BoundsGate("score", min_val=0, max_val=100)
        result = gate.check([])
        assert result.passed is True

    def test_non_numeric_value_fails(self):
        data = [{"score": "abc"}]
        gate = BoundsGate("score", min_val=0, max_val=100)
        result = gate.check(data)
        assert result.passed is False
        assert len(result.failures) == 1
        assert result.failures[0]["reason"] == "not numeric"


class TestUniquenessGateEdgeCases:
    def test_empty_data_passes(self):
        gate = UniquenessGate(["id"])
        result = gate.check([])
        assert result.passed is True


class TestFreshnessGateEdgeCases:
    def test_empty_data_passes(self):
        gate = FreshnessGate("ts", max_age_hours=1)
        result = gate.check([])
        assert result.passed is True

    def test_null_timestamp_skipped(self):
        data = [{"ts": None}]
        gate = FreshnessGate("ts", max_age_hours=1)
        result = gate.check(data)
        assert result.passed is True

    def test_non_numeric_timestamp_skipped(self):
        data = [{"ts": "not_a_timestamp"}]
        gate = FreshnessGate("ts", max_age_hours=1)
        result = gate.check(data)
        assert result.passed is True
