"""S4: Obsługa błędów i autonaprawa (CLI + API).

SCENARIUSZ: System wykrywa błędy i próbuje je naprawić automatycznie.

FLOW:
  1. Tool execution fails → error logged
  2. Watchdog detects error → HealthEvent emitted
  3. If repair_mode=auto → RepairAgent triggered
  4. RepairAgent: diagnose → generate fix → apply → test
  5. If fix works → ticket marked done with repair comment
  6. If fix fails → ticket marked failed, circuit breaker incremented
"""
from __future__ import annotations

import pytest


class TestErrorDetection:
    """S4.1: Wykrywanie błędów."""

    def test_tool_error_appears_in_observability(self, authed_client):
        """Błąd narzędzia widoczny w /observability/errors.

        MUSI:
        - Error w ring buffer po failed job
        - Source identyfikuje narzędzie
        - Traceback (jeśli dostępny) zapisany

        NIE MOŻE:
        - Error zniknąć po restarcie (ring buffer ok, ale musi trwać w sesji)
        - Zawierać API keys w error detail
        """
        r = authed_client.get("/observability/errors")
        assert r.status_code == 200
        data = r.json()
        assert "total_errors" in data
        assert "by_source" in data

    def test_health_events_track_issues(self, authed_client):
        """Health events śledzą aktywne problemy.

        MUSI:
        - GET /observability/health-summary → active_issues count
        - Critical issues mają severity=critical
        - Resolved issues oznaczone resolved=true

        NIE MOŻE:
        - Duplikować ten sam event wielokrotnie
        """
        r = authed_client.get("/observability/health-summary")
        assert r.status_code == 200
        data = r.json()
        assert "active_issues" in data
        assert "critical" in data

    def test_health_events_list(self, authed_client):
        """Lista health events.

        MUSI:
        - GET /observability/health-events → lista
        - Filtrowanie po severity
        """
        r = authed_client.get("/observability/health-events?limit=20")
        assert r.status_code == 200
        assert isinstance(r.json(), list)

    def test_error_summary_structure(self, authed_client):
        """Struktura error summary.

        MUSI:
        - total_errors: int
        - total_logs: int
        - by_source: dict
        - recent_errors: list
        """
        r = authed_client.get("/observability/errors")
        assert r.status_code == 200
        data = r.json()
        assert isinstance(data["total_errors"], int)
        assert isinstance(data["total_logs"], int)
        assert isinstance(data["by_source"], dict)
        assert isinstance(data["recent_errors"], list)


class TestAutoRepair:
    """S4.2: Automatyczna naprawa."""

    def test_repair_suggest_mode(self, authed_client):
        """Tryb suggest — diagnoza bez zmian.

        MUSI:
        - Zwrócić diagnosis (tekst)
        - Zwrócić suggestion (tekst)
        - applied=false
        - Nie modyfikować plików

        NIE MOŻE:
        - Stosować patch
        - Commitować do git
        """
        r = authed_client.post("/observability/repair", json={
            "error_text": "AttributeError: 'NoneType' has no attribute 'model_dump'",
            "file_path": "src/proxym/dashboard/_tickets_api.py",
            "mode": "suggest",
        })
        assert r.status_code == 200
        data = r.json()
        if "error" not in data:  # repair enabled
            assert data.get("applied") is False
            assert len(data.get("diagnosis", "")) > 0
        # If repair disabled, response should explain why
        else:
            assert "disabled" in data["error"].lower() or "key" in data["error"].lower()

    def test_repair_blocked_on_blacklisted_file(self, authed_client):
        """Naprawa config.py zablokowana.

        MUSI:
        - Odmówić naprawy
        - Zwrócić informację o blacklist

        NIE MOŻE:
        - Modyfikować config.py
        - Modyfikować .env
        """
        r = authed_client.post("/observability/repair", json={
            "error_text": "some error",
            "file_path": "src/proxym/config.py",
            "mode": "apply",
        })
        assert r.status_code == 200
        data = r.json()
        # Either repair disabled or blacklist hit — must NOT apply
        assert data.get("applied") is not True

    def test_circuit_breaker_limits_retries(self, authed_client):
        """Max 3 próby na ten sam błąd.

        MUSI:
        - 1-3 próby → normalna odpowiedź
        - 4+ próba → "circuit breaker" w odpowiedzi

        NIE MOŻE:
        - Nieskończone próby naprawy
        """
        for i in range(5):
            r = authed_client.post("/observability/repair", json={
                "error_text": "same error repeated",
                "mode": "suggest",
            })
            assert r.status_code == 200
            data = r.json()
            if i >= 3 and "error" not in data:
                # Circuit breaker should have kicked in
                pass  # OK if repair disabled

    def test_repair_from_logs_diagnoses_top_error(self, authed_client):
        """Auto-diagnoza z ring buffer.

        MUSI:
        - Zebrać najczęstszy błąd
        - Zaproponować diagnozę

        NIE MOŻE:
        - Crashować na pustym buforze
        """
        r = authed_client.post("/observability/repair-from-logs")
        assert r.status_code == 200

    def test_repair_history_endpoint(self, authed_client):
        """Historia napraw.

        MUSI:
        - GET /observability/repair-history → lista
        """
        r = authed_client.get("/observability/repair-history")
        assert r.status_code == 200
        assert isinstance(r.json(), list)


class TestTicketErrorIntegration:
    """S4.3: Ticket + error → pełna ścieżka."""

    def test_failed_ticket_has_error_details(self, client, failed_ticket_id):
        """Ticket z failed ma pełne informacje.

        MUSI:
        - status=failed
        - Ostatni komentarz zawiera error message
        - Komentarz od narzędzia (author=tool_name)

        NIE MOŻE:
        - Status=done gdy execution failed
        - Brak informacji o przyczynie
        """
        r = client.get(f"/dashboard/tickets/{failed_ticket_id}")
        t = r.json()
        assert t["status"] == "cancelled"  # TicketStatus has no 'failed'
        comments = t.get("comments", [])
        assert any(
            "❌" in c.get("content", "") or "failed" in c.get("content", "").lower()
            for c in comments
        )

    def test_auto_repaired_ticket_gets_repair_comment(self, client, repaired_ticket_id):
        """Ticket naprawiony przez LLM ma komentarz o naprawie.

        MUSI:
        - Komentarz od "repair-agent"
        - Zawiera diagnozę
        - Status zmieniony na done (jeśli fix zadziałał)
        """
        r = client.get(f"/dashboard/tickets/{repaired_ticket_id}")
        t = r.json()
        comments = t.get("comments", [])
        repair_comments = [c for c in comments if "repair" in c.get("author", "")]
        # May be empty if repair not enabled, but structure must be correct
        assert isinstance(comments, list)
        if repair_comments:
            assert any("diagnos" in c.get("content", "").lower() for c in repair_comments)
