"""Tests for RepairAgent — context building, circuit breaker, budget, blacklist."""

import time
from unittest.mock import patch, AsyncMock, MagicMock

import pytest

from proxym.observability.health_events import HealthEvent, Severity
from proxym.observability.repair_agent import RepairAgent


@pytest.fixture
def agent(tmp_path):
    return RepairAgent(project_root=str(tmp_path), mode="suggest")


@pytest.fixture
def sample_event():
    return HealthEvent(
        severity=Severity.critical,
        source="proxy",
        title="Proxy health check failed",
        detail="Status 500: Internal Server Error",
    )


# ── Context building ──────────────────────────────────────

class TestBuildContext:
    def test_context_from_event(self, agent, sample_event):
        ctx = agent._build_context(sample_event, "", "", "")
        assert ctx["title"] == "Proxy health check failed"
        assert ctx["source"] == "proxy"
        assert "500" in ctx["error"]

    def test_context_with_error_text(self, agent):
        ctx = agent._build_context(None, "ImportError: no module named foo", "", "")
        assert "ImportError" in ctx["error"]

    def test_context_reads_source_file(self, agent, tmp_path):
        src = tmp_path / "test_file.py"
        src.write_text("def hello(): pass\n")
        ctx = agent._build_context(None, "error", "test_file.py", "")
        assert "source_code" in ctx
        assert "hello" in ctx["source_code"]

    def test_context_skips_blacklisted_file(self, agent, tmp_path):
        # config.py is blacklisted
        (tmp_path / "src" / "proxym").mkdir(parents=True)
        cfg = tmp_path / "src" / "proxym" / "config.py"
        cfg.write_text("SECRET=123\n")
        ctx = agent._build_context(None, "error", "src/proxym/config.py", "")
        assert "source_code" not in ctx

    def test_context_skips_missing_file(self, agent):
        ctx = agent._build_context(None, "error", "nonexistent.py", "")
        assert "source_code" not in ctx


class TestBuildPrompt:
    def test_prompt_contains_error(self, agent, sample_event):
        ctx = agent._build_context(sample_event, "", "", "")
        prompt = agent._build_prompt(ctx)
        assert "Proxy health check failed" in prompt
        assert "proxym" in prompt.lower()

    def test_prompt_contains_traceback(self, agent):
        ctx = agent._build_context(None, "err", "", "Traceback: line 42")
        prompt = agent._build_prompt(ctx)
        assert "Traceback: line 42" in prompt

    def test_prompt_asks_for_json(self, agent):
        ctx = agent._build_context(None, "err", "", "")
        prompt = agent._build_prompt(ctx)
        assert "JSON" in prompt


# ── Circuit breaker ───────────────────────────────────────

class TestCircuitBreaker:
    @pytest.mark.asyncio
    async def test_blocks_after_3_attempts(self, agent, sample_event):
        # Mock _call_llm to return empty result
        agent._call_llm = AsyncMock(return_value={"diagnosis": "test", "suggestion": ""})

        with patch.object(agent, '_call_llm', new_callable=AsyncMock,
                          return_value={"diagnosis": "x", "suggestion": "y"}):
            with patch("proxym.config.get_settings") as mock_settings:
                s = MagicMock()
                s.repair_model = "test-model"
                s.repair_api_key = "sk-test"
                s.openrouter_api_key = "sk-test"
                s.repair_budget_daily = 10.0
                mock_settings.return_value = s

                # 3 attempts should work
                for _ in range(3):
                    result = await agent.diagnose_and_repair(event=sample_event)

                # 4th should be blocked
                result = await agent.diagnose_and_repair(event=sample_event)
                assert "Circuit breaker" in result.get("error", "")


# ── Budget guard ──────────────────────────────────────────

class TestBudgetGuard:
    @pytest.mark.asyncio
    async def test_blocks_when_budget_exhausted(self, agent, sample_event):
        agent._daily_cost = 99.0  # way over budget

        with patch("proxym.config.get_settings") as mock_settings:
            s = MagicMock()
            s.repair_model = "test"
            s.repair_api_key = "sk-test"
            s.openrouter_api_key = "sk-test"
            s.repair_budget_daily = 2.0
            mock_settings.return_value = s

            result = await agent.diagnose_and_repair(event=sample_event)
            assert "budget" in result.get("error", "").lower()

    def test_daily_budget_resets(self, agent):
        agent._daily_cost = 5.0
        agent._daily_cost_reset = time.time() - 90000  # >24h ago
        agent._maybe_reset_daily_budget()
        assert agent._daily_cost == 0.0


# ── Blacklist ─────────────────────────────────────────────

class TestBlacklist:
    def test_config_py_is_blacklisted(self):
        assert "src/proxym/config.py" in RepairAgent.BLACKLISTED_FILES

    def test_env_is_blacklisted(self):
        assert ".env" in RepairAgent.BLACKLISTED_FILES

    def test_docker_compose_is_blacklisted(self):
        assert "docker-compose.yml" in RepairAgent.BLACKLISTED_FILES


# ── History ───────────────────────────────────────────────

class TestHistory:
    def test_empty_history(self, agent):
        assert agent.get_history() == []

    @pytest.mark.asyncio
    async def test_history_populated_after_diagnosis(self, agent, sample_event):
        with patch.object(agent, '_call_llm', new_callable=AsyncMock,
                          return_value={"diagnosis": "test bug", "suggestion": "fix it"}):
            with patch("proxym.config.get_settings") as mock_settings:
                s = MagicMock()
                s.repair_model = "test"
                s.repair_api_key = "sk-test"
                s.openrouter_api_key = "sk-test"
                s.repair_budget_daily = 10.0
                mock_settings.return_value = s

                await agent.diagnose_and_repair(event=sample_event)
                history = agent.get_history()
                assert len(history) == 1
                assert history[0]["diagnosis"] == "test bug"


# ── No API key ────────────────────────────────────────────

class TestNoApiKey:
    @pytest.mark.asyncio
    async def test_returns_error_without_key(self, agent, sample_event):
        with patch("proxym.observability.repair_agent.get_settings") as mock_settings:
            s = MagicMock()
            s.repair_model = ""
            s.repair_api_key = ""
            s.openrouter_api_key = ""
            mock_settings.return_value = s

            result = await agent.diagnose_and_repair(event=sample_event)
            assert "error" in result
            assert "key" in result["error"].lower() or "KEY" in result["error"]
