"""Tests for healing pipeline — models, detector, diagnoser, healer."""

import pytest
from uuid import uuid4

from xyglue.heal.models import (
    ErrorSignature, Failure, Fix, Diagnosis, HealResult,
    SEVERITY_MAP, ERROR_TYPE_SEVERITY,
)
from xyglue.heal.detector import FailureDetector
from xyglue.heal.diagnoser import Diagnoser
from xyglue.heal.healer import Healer
from xyglue.plan.models import Step
from xyglue.execute.models import ExecutionResult
from xyglue.learn.patterns import PatternLibrary
from xyglue.learn.seeds import seed_patterns


class TestErrorSignature:
    def test_to_string(self):
        sig = ErrorSignature(error_type="timeout", tool="http", action="fetch")
        assert sig.to_string() == "timeout:http:fetch:"

    def test_from_string(self):
        sig = ErrorSignature.from_string("timeout:http:fetch:deadline exceeded")
        assert sig.error_type == "timeout"
        assert sig.tool == "http"
        assert sig.message_pattern == "deadline exceeded"

    def test_from_error(self):
        sig = ErrorSignature.from_error("network", "http", "get", "connection refused")
        assert sig.error_type == "network"
        assert sig.message_pattern == "connection refused"

    def test_matches_at_level_exact(self):
        sig1 = ErrorSignature(error_type="timeout", tool="http", action="get", message_pattern="slow")
        sig2 = ErrorSignature(error_type="timeout", tool="http", action="get", message_pattern="slow")
        assert sig1.matches_at_level(sig2, "exact") is True

    def test_matches_at_level_generic(self):
        sig1 = ErrorSignature(error_type="timeout", tool="http")
        sig2 = ErrorSignature(error_type="timeout", tool="db")
        assert sig1.matches_at_level(sig2, "generic") is True

    def test_to_dict_from_dict(self):
        sig = ErrorSignature(error_type="auth", tool="api")
        d = sig.to_dict()
        sig2 = ErrorSignature.from_dict(d)
        assert sig2.error_type == "auth"
        assert sig2.tool == "api"


class TestFailure:
    def test_basic_failure(self):
        f = Failure(error_type="timeout", tool="http", error_message="timed out")
        assert f.error_type == "timeout"
        assert f.recoverable is True

    def test_severity_auto_assigned(self):
        f = Failure(error_type="auth")
        assert f.severity == "high"  # Auto from ERROR_TYPE_SEVERITY

    def test_hierarchical_signatures(self):
        f = Failure(error_type="timeout", tool="http", action="get", error_message="slow")
        sigs = f.get_hierarchical_signatures()
        assert "timeout" in sigs["generic"]
        assert "http" in sigs["action"]
        assert "get" in sigs["domain"]

    def test_get_error_signature(self):
        f = Failure(error_type="network", tool="api", action="call", error_message="refused")
        sig = f.get_error_signature()
        assert sig.error_type == "network"

    def test_is_critical(self):
        f = Failure(error_type="auth")
        assert f.is_critical is True
        f2 = Failure(error_type="timeout")
        assert f2.is_critical is False

    def test_to_dict_from_dict(self):
        f = Failure(error_type="network", tool="http", error_message="refused")
        d = f.to_dict()
        f2 = Failure.from_dict(d)
        assert f2.error_type == "network"
        assert f2.tool == "http"


class TestFix:
    def test_basic_fix(self):
        f = Fix(fix_type="retry", confidence=0.8)
        assert f.is_retry is True
        assert f.is_escalation is False
        assert f.is_high_confidence is True

    def test_retry_delay(self):
        f = Fix(fix_type="retry", fix_params={"delay_ms": 5000})
        assert f.get_retry_delay_ms() == 5000

    def test_timeout_multiplier(self):
        f = Fix(fix_type="retry", fix_params={"multiply_timeout": 2})
        assert f.get_timeout_multiplier() == 2.0

    def test_mark_applied(self):
        f = Fix(fix_type="retry")
        assert f.applied_at is None
        f.mark_applied()
        assert f.applied_at is not None

    def test_to_dict_from_dict(self):
        f = Fix(fix_type="modify_params", source="pattern", confidence=0.7)
        d = f.to_dict()
        f2 = Fix.from_dict(d)
        assert f2.fix_type == "modify_params"
        assert f2.source == "pattern"


class TestDiagnosis:
    def test_to_fix(self):
        failure = Failure(error_type="timeout")
        d = Diagnosis(failure=failure, fix_type="retry", confidence=0.7, root_cause="slow")
        fix = d.to_fix()
        assert fix.fix_type == "retry"
        assert fix.confidence == 0.7

    def test_alternatives(self):
        d = Diagnosis(
            alternatives=[
                {"fix_type": "retry", "confidence": 0.5},
                {"fix_type": "skip", "confidence": 0.8},
            ]
        )
        assert d.has_alternatives is True
        best = d.best_alternative()
        assert best["confidence"] == 0.8

    def test_to_dict_from_dict(self):
        d = Diagnosis(root_cause="timeout", fix_type="retry", source="ai")
        data = d.to_dict()
        d2 = Diagnosis.from_dict(data)
        assert d2.root_cause == "timeout"
        assert d2.source == "ai"


class TestHealResult:
    def test_failed_result(self):
        r = HealResult.failed("no fix available")
        assert r.success is False
        assert "no fix" in r.message

    def test_succeeded_result(self):
        fix = Fix(fix_type="retry", confidence=0.8)
        r = HealResult.succeeded(fix, "retrying", duration_ms=50)
        assert r.success is True
        assert r.fix_type == "retry"
        assert r.confidence == 0.8
        assert r.is_retry is True

    def test_to_dict_from_dict(self):
        fix = Fix(fix_type="skip")
        r = HealResult(success=True, fix_applied=fix, message="skipped")
        d = r.to_dict()
        r2 = HealResult.from_dict(d)
        assert r2.success is True


class TestFailureDetector:
    @pytest.fixture
    def detector(self):
        return FailureDetector()

    def test_detect_success_returns_none(self, detector):
        result = ExecutionResult(success=True)
        step = Step(number=1, tool="http")
        failure = detector.detect(uuid4(), step, result)
        assert failure is None

    def test_detect_timeout(self, detector):
        result = ExecutionResult(success=False, error="Request timed out")
        step = Step(number=1, tool="http", action="fetch")
        failure = detector.detect(uuid4(), step, result)
        assert failure is not None
        assert failure.error_type == "timeout"

    def test_detect_auth(self, detector):
        result = ExecutionResult(success=False, error="401 Unauthorized")
        step = Step(number=1, tool="api")
        failure = detector.detect(uuid4(), step, result)
        assert failure is not None
        assert failure.error_type == "auth"
        assert failure.recoverable is False

    def test_detect_network(self, detector):
        result = ExecutionResult(success=False, error="ECONNREFUSED")
        step = Step(number=1, tool="http")
        failure = detector.detect(uuid4(), step, result)
        assert failure is not None
        assert failure.error_type == "network"

    def test_detect_rate_limit(self, detector):
        result = ExecutionResult(success=False, error="429 Too Many Requests")
        step = Step(number=1, tool="api")
        failure = detector.detect(uuid4(), step, result)
        assert failure is not None
        assert failure.error_type == "rate_limit"

    def test_detect_unknown(self, detector):
        result = ExecutionResult(success=False, error="Something weird happened")
        step = Step(number=1, tool="custom")
        failure = detector.detect(uuid4(), step, result)
        assert failure is not None
        assert failure.error_type == "unknown"

    def test_classify_error_public(self, detector):
        assert detector.classify_error("connection reset") == "network"
        assert detector.classify_error("not found") == "not_found"

    def test_batch_detection(self, detector):
        run_id = uuid4()
        steps = [Step(number=1, tool="a"), Step(number=2, tool="b")]
        results = [
            ExecutionResult(success=True),
            ExecutionResult(success=False, error="timeout"),
        ]
        failures = detector.detect_batch(run_id, steps, results)
        assert len(failures) == 1
        assert failures[0].error_type == "timeout"

    def test_cascade_detection(self, detector):
        f1 = Failure(step_number=1, error_type="network", tool="http")
        f2 = Failure(step_number=2, error_type="timeout", tool="http")
        f3 = Failure(step_number=5, error_type="auth", tool="api")
        chains = detector.detect_cascade([f1, f2, f3])
        assert len(chains) >= 1

    def test_stats(self, detector):
        result = ExecutionResult(success=False, error="timeout")
        step = Step(number=1, tool="http")
        detector.detect(uuid4(), step, result)
        stats = detector.get_stats()
        assert stats["total_detected"] == 1

    def test_is_retryable(self, detector):
        assert detector.is_retryable("timeout") is True
        assert detector.is_retryable("auth") is False


class TestDiagnoser:
    @pytest.fixture
    def diagnoser(self):
        return Diagnoser()

    async def test_diagnose_timeout(self, diagnoser):
        failure = Failure(error_type="timeout", tool="http")
        result = await diagnoser.diagnose(failure)
        assert result["fix_type"] == "retry"
        assert result["confidence"] >= 0.5

    async def test_diagnose_unknown(self, diagnoser):
        failure = Failure(error_type="unknown_weird", tool="custom")
        result = await diagnoser.diagnose(failure)
        assert "fix_type" in result
        assert result["confidence"] > 0

    async def test_diagnose_full(self, diagnoser):
        failure = Failure(error_type="network", tool="http")
        diag = await diagnoser.diagnose_full(failure)
        assert isinstance(diag, Diagnosis)
        assert diag.fix_type in ("retry", "modify_params", "skip", "escalate", "alternative_tool")

    async def test_diagnose_multi_strategy(self, diagnoser):
        failure = Failure(error_type="timeout", tool="database")
        strategies = await diagnoser.diagnose_multi_strategy(failure)
        assert len(strategies) >= 1
        # Should be sorted by confidence
        confs = [s["confidence"] for s in strategies]
        assert confs == sorted(confs, reverse=True)

    async def test_tool_specific_fix(self, diagnoser):
        failure = Failure(error_type="timeout", tool="database")
        result = await diagnoser.diagnose(failure)
        # Should get tool-specific fix with reconnect
        assert result["confidence"] >= 0.5

    def test_stats(self, diagnoser):
        stats = diagnoser.get_stats()
        assert stats["total_diagnoses"] == 0


class TestHealer:
    @pytest.fixture
    def healer(self):
        lib = PatternLibrary()
        seed_patterns(lib)
        return Healer(pattern_library=lib)

    async def test_heal_success(self, healer):
        step = Step(number=1, tool="http", action="fetch")
        result = ExecutionResult(success=False, error="connection timed out")
        heal_result = await healer.heal(uuid4(), step, result)
        assert heal_result.success is True
        assert heal_result.fix_applied is not None

    async def test_heal_no_failure(self, healer):
        step = Step(number=1, tool="http")
        result = ExecutionResult(success=True)
        heal_result = await healer.heal(uuid4(), step, result)
        assert heal_result.success is False

    async def test_pattern_match_first(self, healer):
        step = Step(number=1, tool="http", action="get")
        result = ExecutionResult(success=False, error="rate limit exceeded")
        heal_result = await healer.heal(uuid4(), step, result)
        assert heal_result.success is True
        assert heal_result.fix_applied.source == "pattern"

    def test_record_fix_outcome(self, healer):
        fix = Fix(fix_type="retry", source="pattern", pattern_id=uuid4(), confidence=0.8)
        fix.failure = Failure(error_type="timeout", signature="timeout:http:")
        # Should not raise
        healer.record_fix_outcome(fix, success=True)

    def test_can_heal(self, healer):
        assert healer.can_heal() is True
        healer.heal_count = 3
        assert healer.can_heal() is False

    def test_reset_for_step(self, healer):
        healer.heal_count = 2
        healer.reset_for_step()
        assert healer.heal_count == 0
