"""Tests for fault injection."""

from litmus.models import Trace, LLMCall, LLMRequest, LLMResponse, TraceMetadata
from litmus.faults import Fault, FaultType, apply_faults, parse_fault_string


def _make_trace():
    trace = Trace(metadata=TraceMetadata())
    for i in range(3):
        trace.add_step(LLMCall(
            request=LLMRequest(provider="anthropic", model="claude-sonnet-4-20250514", method="POST", path="/v1/messages"),
            response=LLMResponse(
                status_code=200,
                body={"content": [{"type": "text", "text": f"response {i}"}], "stop_reason": "end_turn"},
            ),
            latency_ms=500,
        ))
    return trace


class TestFaultInjection:
    def test_llm_error_single_step(self):
        trace = _make_trace()
        faulted = apply_faults(trace, [Fault(FaultType.LLM_ERROR, step=1)])

        # Original unchanged
        assert trace.steps[1].response.status_code == 200

        # Faulted step has 500
        assert faulted.steps[1].response.status_code == 500
        assert faulted.steps[1].error is not None

        # Other steps unchanged
        assert faulted.steps[0].response.status_code == 200
        assert faulted.steps[2].response.status_code == 200

    def test_llm_refuse(self):
        trace = _make_trace()
        faulted = apply_faults(trace, [Fault(FaultType.LLM_REFUSE, step=0)])

        assert faulted.steps[0].response.status_code == 200  # Refusal is 200
        body = faulted.steps[0].response.body
        assert "sorry" in body["content"][0]["text"].lower()

    def test_llm_timeout(self):
        trace = _make_trace()
        faulted = apply_faults(trace, [Fault(FaultType.LLM_TIMEOUT, step=0, delay_ms=5000)])

        assert faulted.steps[0].response.status_code == 408
        assert faulted.steps[0].latency_ms == 5000

    def test_llm_hallucinate(self):
        trace = _make_trace()
        faulted = apply_faults(trace, [Fault(FaultType.LLM_HALLUCINATE, step=0)])

        body = faulted.steps[0].response.body
        assert "42" in body["content"][0]["text"]

    def test_multiple_faults(self):
        trace = _make_trace()
        faulted = apply_faults(trace, [
            Fault(FaultType.LLM_ERROR, step=0),
            Fault(FaultType.LLM_REFUSE, step=2),
        ])

        assert faulted.steps[0].response.status_code == 500
        assert faulted.steps[1].response.status_code == 200  # untouched
        assert "sorry" in faulted.steps[2].response.body["content"][0]["text"].lower()

    def test_original_not_mutated(self):
        trace = _make_trace()
        original_body = trace.steps[0].response.body.copy()
        apply_faults(trace, [Fault(FaultType.LLM_HALLUCINATE, step=0)])
        assert trace.steps[0].response.body == original_body

    def test_parse_fault_string(self):
        f = parse_fault_string("llm_refuse:step=2")
        assert f.fault_type == FaultType.LLM_REFUSE
        assert f.step == 2

    def test_parse_fault_string_no_step(self):
        f = parse_fault_string("llm_error")
        assert f.fault_type == FaultType.LLM_ERROR
        assert f.step is None

    def test_parse_fault_string_with_delay(self):
        f = parse_fault_string("llm_timeout:step=0,delay_ms=10000")
        assert f.fault_type == FaultType.LLM_TIMEOUT
        assert f.step == 0
        assert f.delay_ms == 10000
