"""Unit tests for Google Ads _monitoring.py.

Mock-based tests for _MonitoringMixin's evaluate_delivery_goal,
evaluate_cpa_goal, evaluate_cv_goal, and diagnose_zero_conversions.
"""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock

import pytest

from mureo.google_ads._monitoring import _MonitoringMixin


# ---------------------------------------------------------------------------
# Mock client class for tests
# ---------------------------------------------------------------------------


class _MockMonitoringClient(_MonitoringMixin):
    """Mock class that makes _MonitoringMixin testable."""

    def __init__(self) -> None:
        self._customer_id = "1234567890"
        self._client = MagicMock()

    @staticmethod
    def _validate_id(value: str, field_name: str) -> str:
        if not value or not value.isdigit():
            raise ValueError(f"{field_name} must be a numeric string: {value}")
        return value

    async def get_campaign(self, campaign_id: str):
        return None

    async def get_performance_report(self, **kwargs):
        return []

    async def diagnose_campaign_delivery(self, campaign_id: str):
        return {}

    async def analyze_performance(self, campaign_id: str, period: str = "LAST_7_DAYS"):
        return {}

    async def investigate_cost_increase(self, campaign_id: str):
        return {}

    async def get_search_terms_report(self, **kwargs):
        return []

    async def list_conversion_actions(self):
        return []


# ---------------------------------------------------------------------------
# evaluate_delivery_goal tests
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestEvaluateDeliveryGoal:
    @pytest.fixture()
    def client(self) -> _MockMonitoringClient:
        return _MockMonitoringClient()

    @pytest.mark.asyncio
    async def test_healthy_campaign(self, client: _MockMonitoringClient) -> None:
        """Healthy delivery → healthy."""
        client.get_campaign = AsyncMock(return_value={"status": "ENABLED"})
        client.diagnose_campaign_delivery = AsyncMock(
            return_value={
                "issues": [],
                "warnings": [],
            }
        )
        client.get_performance_report = AsyncMock(
            return_value=[{"metrics": {"impressions": 100, "clicks": 10}}]
        )

        result = await client.evaluate_delivery_goal("123")
        assert result["status"] == "healthy"
        assert "normally" in result["summary"]

    @pytest.mark.asyncio
    async def test_critical_no_impressions(self, client: _MockMonitoringClient) -> None:
        """Zero impressions → critical."""
        client.get_campaign = AsyncMock(return_value={"status": "ENABLED"})
        client.diagnose_campaign_delivery = AsyncMock(
            return_value={
                "issues": [],
                "warnings": [],
            }
        )
        client.get_performance_report = AsyncMock(
            return_value=[{"metrics": {"impressions": 0, "clicks": 0}}]
        )

        result = await client.evaluate_delivery_goal("123")
        assert result["status"] == "critical"
        assert result.get("suggested_workflow") == "delivery_fix"

    @pytest.mark.asyncio
    async def test_critical_with_issues(self, client: _MockMonitoringClient) -> None:
        """Diagnostic issues detected → critical."""
        client.get_campaign = AsyncMock(return_value={"status": "ENABLED"})
        client.diagnose_campaign_delivery = AsyncMock(
            return_value={
                "issues": ["有効な広告がありません"],
                "warnings": [],
            }
        )
        client.get_performance_report = AsyncMock(
            return_value=[{"metrics": {"impressions": 50, "clicks": 5}}]
        )

        result = await client.evaluate_delivery_goal("123")
        assert result["status"] == "critical"

    @pytest.mark.asyncio
    async def test_warning_with_warnings_only(
        self, client: _MockMonitoringClient
    ) -> None:
        """Only warnings from the diagnostic → warning."""
        client.get_campaign = AsyncMock(return_value={"status": "ENABLED"})
        client.diagnose_campaign_delivery = AsyncMock(
            return_value={
                "issues": [],
                "warnings": ["地域ターゲティングが未設定"],
            }
        )
        client.get_performance_report = AsyncMock(
            return_value=[{"metrics": {"impressions": 50, "clicks": 5}}]
        )

        result = await client.evaluate_delivery_goal("123")
        assert result["status"] == "warning"

    @pytest.mark.asyncio
    async def test_paused_campaign_critical(
        self, client: _MockMonitoringClient
    ) -> None:
        """Paused → critical."""
        client.get_campaign = AsyncMock(return_value={"status": "PAUSED"})
        client.diagnose_campaign_delivery = AsyncMock(
            return_value={
                "issues": [],
                "warnings": [],
            }
        )
        client.get_performance_report = AsyncMock(
            return_value=[{"metrics": {"impressions": 0, "clicks": 0}}]
        )

        result = await client.evaluate_delivery_goal("123")
        assert result["status"] == "critical"

    @pytest.mark.asyncio
    async def test_exception_handling(self, client: _MockMonitoringClient) -> None:
        """Exceptions from each method are swallowed."""
        client.get_campaign = AsyncMock(side_effect=RuntimeError("fail"))
        client.diagnose_campaign_delivery = AsyncMock(side_effect=RuntimeError("fail"))
        client.get_performance_report = AsyncMock(side_effect=RuntimeError("fail"))

        result = await client.evaluate_delivery_goal("123")
        assert result["status"] in ("critical", "warning")


# ---------------------------------------------------------------------------
# evaluate_cpa_goal tests
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestEvaluateCpaGoal:
    @pytest.fixture()
    def client(self) -> _MockMonitoringClient:
        return _MockMonitoringClient()

    @pytest.mark.asyncio
    async def test_healthy_cpa(self, client: _MockMonitoringClient) -> None:
        """Within the CPA target → healthy."""
        client.get_performance_report = AsyncMock(
            return_value=[{"metrics": {"cost": 10000, "conversions": 10}}]
        )
        client.investigate_cost_increase = AsyncMock(return_value={})

        result = await client.evaluate_cpa_goal("123", 2000.0)
        assert result["status"] == "healthy"
        assert result["current_cpa"] == 1000.0

    @pytest.mark.asyncio
    async def test_warning_cpa(self, client: _MockMonitoringClient) -> None:
        """At or below 1.2× the CPA target → warning."""
        client.get_performance_report = AsyncMock(
            return_value=[{"metrics": {"cost": 11000, "conversions": 10}}]  # CPA=1100
        )
        client.investigate_cost_increase = AsyncMock(return_value={})

        result = await client.evaluate_cpa_goal("123", 1000.0)
        assert result["status"] == "warning"
        assert result["deviation_pct"] > 0

    @pytest.mark.asyncio
    async def test_critical_cpa(self, client: _MockMonitoringClient) -> None:
        """Above 1.2× the CPA target → critical."""
        client.get_performance_report = AsyncMock(
            return_value=[{"metrics": {"cost": 15000, "conversions": 10}}]  # CPA=1500
        )
        client.investigate_cost_increase = AsyncMock(return_value={})

        result = await client.evaluate_cpa_goal("123", 1000.0)
        assert result["status"] == "critical"
        assert result.get("suggested_workflow") == "cpa_optimization"

    @pytest.mark.asyncio
    async def test_zero_conversions(self, client: _MockMonitoringClient) -> None:
        """CV0 → warning, current_cpa=None"""
        client.get_performance_report = AsyncMock(
            return_value=[{"metrics": {"cost": 5000, "conversions": 0}}]
        )
        client.investigate_cost_increase = AsyncMock(return_value={})

        result = await client.evaluate_cpa_goal("123", 1000.0)
        assert result["status"] == "warning"
        assert result["current_cpa"] is None

    @pytest.mark.asyncio
    async def test_wasteful_terms_extraction(
        self, client: _MockMonitoringClient
    ) -> None:
        """wasteful_search_terms are extracted correctly."""
        client.get_performance_report = AsyncMock(
            return_value=[{"metrics": {"cost": 5000, "conversions": 5}}]
        )
        client.investigate_cost_increase = AsyncMock(
            return_value={
                "wasteful_search_terms": [{"term": f"t{i}"} for i in range(10)]
            }
        )

        result = await client.evaluate_cpa_goal("123", 2000.0)
        assert len(result["wasteful_terms"]) == 5  # top 5


# ---------------------------------------------------------------------------
# evaluate_cv_goal tests
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestEvaluateCvGoal:
    @pytest.fixture()
    def client(self) -> _MockMonitoringClient:
        return _MockMonitoringClient()

    @pytest.mark.asyncio
    async def test_healthy_cv(self, client: _MockMonitoringClient) -> None:
        """CV target met → healthy."""
        client.get_performance_report = AsyncMock(
            return_value=[
                {"metrics": {"impressions": 1000, "clicks": 100, "conversions": 70}}
            ]
        )
        client.analyze_performance = AsyncMock(return_value={"insights": []})

        result = await client.evaluate_cv_goal("123", 10.0)
        assert result["status"] == "healthy"
        assert result["current_cv_daily"] == 10.0  # 70/7

    @pytest.mark.asyncio
    async def test_warning_cv(self, client: _MockMonitoringClient) -> None:
        """At or above 80% of the CV target → warning."""
        client.get_performance_report = AsyncMock(
            return_value=[
                {"metrics": {"impressions": 1000, "clicks": 100, "conversions": 63}}
            ]
        )
        client.analyze_performance = AsyncMock(return_value={"insights": []})

        result = await client.evaluate_cv_goal("123", 10.0)
        assert result["status"] == "warning"

    @pytest.mark.asyncio
    async def test_critical_cv(self, client: _MockMonitoringClient) -> None:
        """Below 80% of the CV target → critical."""
        client.get_performance_report = AsyncMock(
            return_value=[
                {"metrics": {"impressions": 1000, "clicks": 100, "conversions": 35}}
            ]
        )
        client.analyze_performance = AsyncMock(return_value={"insights": []})

        result = await client.evaluate_cv_goal("123", 10.0)
        assert result["status"] == "critical"
        assert result.get("suggested_workflow") == "cv_increase"

    @pytest.mark.asyncio
    async def test_bottleneck_impression(self, client: _MockMonitoringClient) -> None:
        """Impression-related insight → impression bottleneck."""
        client.get_performance_report = AsyncMock(
            return_value=[
                {"metrics": {"impressions": 10, "clicks": 5, "conversions": 0}}
            ]
        )
        client.analyze_performance = AsyncMock(
            return_value={"insights": ["インプレッションが不足しています"]}
        )

        result = await client.evaluate_cv_goal("123", 10.0)
        assert result["bottleneck"] == "impression"

    @pytest.mark.asyncio
    async def test_bottleneck_ctr(self, client: _MockMonitoringClient) -> None:
        """Low CTR → CTR bottleneck."""
        client.get_performance_report = AsyncMock(
            return_value=[
                {"metrics": {"impressions": 10000, "clicks": 10, "conversions": 0}}
            ]
        )
        client.analyze_performance = AsyncMock(return_value={"insights": []})

        result = await client.evaluate_cv_goal("123", 10.0)
        assert result["bottleneck"] == "ctr"

    @pytest.mark.asyncio
    async def test_bottleneck_cvr(self, client: _MockMonitoringClient) -> None:
        """Low CVR → CVR bottleneck."""
        client.get_performance_report = AsyncMock(
            return_value=[
                {"metrics": {"impressions": 10000, "clicks": 500, "conversions": 1}}
            ]
        )
        client.analyze_performance = AsyncMock(return_value={"insights": []})

        result = await client.evaluate_cv_goal("123", 10.0)
        assert result["bottleneck"] == "cvr"

    @pytest.mark.asyncio
    async def test_zero_target(self, client: _MockMonitoringClient) -> None:
        """target_cv_daily=0 → deviation_pct=0"""
        client.get_performance_report = AsyncMock(
            return_value=[
                {"metrics": {"impressions": 100, "clicks": 10, "conversions": 0}}
            ]
        )
        client.analyze_performance = AsyncMock(return_value={"insights": []})

        result = await client.evaluate_cv_goal("123", 0.0)
        assert result["deviation_pct"] == 0.0


# ---------------------------------------------------------------------------
# diagnose_zero_conversions tests
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestDiagnoseZeroConversions:
    @pytest.fixture()
    def client(self) -> _MockMonitoringClient:
        return _MockMonitoringClient()

    @pytest.mark.asyncio
    async def test_no_cv_tracking_critical(self, client: _MockMonitoringClient) -> None:
        """No CV measurement configured → critical."""
        client.get_campaign = AsyncMock(
            return_value={"bidding_strategy": "MAXIMIZE_CONVERSIONS"}
        )
        client.list_conversion_actions = AsyncMock(return_value=[])
        client.get_performance_report = AsyncMock(
            return_value=[
                {
                    "metrics": {
                        "impressions": 100,
                        "clicks": 10,
                        "conversions": 0,
                        "cost": 5000,
                    }
                }
            ]
        )
        client.diagnose_campaign_delivery = AsyncMock(
            return_value={
                "issues": [],
                "warnings": [],
                "recommendations": [],
            }
        )

        result = await client.diagnose_zero_conversions("123")
        assert result["status"] == "critical"
        assert result["conversion_tracking"]["has_issue"] is True

    @pytest.mark.asyncio
    async def test_no_delivery_bottleneck(self, client: _MockMonitoringClient) -> None:
        """Zero impressions → no_delivery bottleneck."""
        client.get_campaign = AsyncMock(
            return_value={"bidding_strategy": "MAXIMIZE_CLICKS"}
        )
        client.list_conversion_actions = AsyncMock(return_value=[{"status": "ENABLED"}])
        client.get_performance_report = AsyncMock(
            return_value=[
                {
                    "metrics": {
                        "impressions": 0,
                        "clicks": 0,
                        "conversions": 0,
                        "cost": 0,
                    }
                }
            ]
        )
        client.diagnose_campaign_delivery = AsyncMock(
            return_value={
                "issues": [],
                "warnings": [],
                "recommendations": [],
            }
        )

        result = await client.diagnose_zero_conversions("123")
        assert result["funnel"]["bottleneck"] == "no_delivery"

    @pytest.mark.asyncio
    async def test_no_clicks_bottleneck(self, client: _MockMonitoringClient) -> None:
        """Zero clicks → no_clicks bottleneck."""
        client.get_campaign = AsyncMock(
            return_value={"bidding_strategy": "MAXIMIZE_CLICKS"}
        )
        client.list_conversion_actions = AsyncMock(return_value=[{"status": "ENABLED"}])
        client.get_performance_report = AsyncMock(
            return_value=[
                {
                    "metrics": {
                        "impressions": 100,
                        "clicks": 0,
                        "conversions": 0,
                        "cost": 0,
                    }
                }
            ]
        )
        client.diagnose_campaign_delivery = AsyncMock(
            return_value={
                "issues": [],
                "warnings": [],
                "recommendations": [],
            }
        )

        result = await client.diagnose_zero_conversions("123")
        assert result["funnel"]["bottleneck"] == "no_clicks"

    @pytest.mark.asyncio
    async def test_healthy_with_conversions(
        self, client: _MockMonitoringClient
    ) -> None:
        """With conversions → healthy."""
        client.get_campaign = AsyncMock(
            return_value={"bidding_strategy": "MAXIMIZE_CLICKS"}
        )
        client.list_conversion_actions = AsyncMock(return_value=[{"status": "ENABLED"}])
        client.get_performance_report = AsyncMock(
            return_value=[
                {
                    "metrics": {
                        "impressions": 1000,
                        "clicks": 100,
                        "conversions": 10,
                        "cost": 50000,
                    }
                }
            ]
        )
        client.diagnose_campaign_delivery = AsyncMock(
            return_value={
                "issues": [],
                "warnings": [],
                "recommendations": [],
            }
        )

        result = await client.diagnose_zero_conversions("123")
        assert result["status"] == "healthy"

    @pytest.mark.asyncio
    async def test_search_term_quality_high_waste(
        self, client: _MockMonitoringClient
    ) -> None:
        """If zero-conversion search terms account for >50% of cost, it is an issue."""
        client.get_campaign = AsyncMock(
            return_value={"bidding_strategy": "MAXIMIZE_CLICKS"}
        )
        client.list_conversion_actions = AsyncMock(return_value=[{"status": "ENABLED"}])
        client.get_performance_report = AsyncMock(
            return_value=[
                {
                    "metrics": {
                        "impressions": 1000,
                        "clicks": 100,
                        "conversions": 0,
                        "cost": 10000,
                    }
                }
            ]
        )
        client.diagnose_campaign_delivery = AsyncMock(
            return_value={
                "issues": [],
                "warnings": [],
                "recommendations": [],
            }
        )
        client.get_search_terms_report = AsyncMock(
            return_value=[
                {"search_term": "bad1", "metrics": {"conversions": 0, "cost": 6000}},
                {"search_term": "bad2", "metrics": {"conversions": 0, "cost": 2000}},
            ]
        )

        result = await client.diagnose_zero_conversions("123")
        assert result["search_term_quality"] is not None
        assert result["search_term_quality"]["zero_cv_cost"] == 8000


# ---------------------------------------------------------------------------
# _build_cv_recommendations tests
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestBuildCvRecommendations:
    def test_all_issues(self) -> None:
        """Recommended actions when all problems are present."""
        actions = _MonitoringMixin._build_cv_recommendations(
            has_cv_issue=True,
            bidding_issue="入札戦略不整合",
            bottleneck="no_delivery",
            search_term_quality={"zero_cv_terms": 5},
            cost=5000.0,
        )
        action_types = [a["action"] for a in actions]
        assert "fix_cv_tracking" in action_types
        assert "fix_bidding_strategy" in action_types
        assert "add_negative_keywords" in action_types
        assert "fix_delivery" in action_types
        # Priority should be ascending.
        priorities = [a["priority"] for a in actions]
        assert priorities == sorted(priorities)

    def test_no_issues(self) -> None:
        """Basic suggestions are included even when there are no problems."""
        actions = _MonitoringMixin._build_cv_recommendations(
            has_cv_issue=False,
            bidding_issue=None,
            bottleneck=None,
            search_term_quality=None,
            cost=0.0,
        )
        action_types = [a["action"] for a in actions]
        assert "improve_ads_and_keywords" in action_types
        assert "review_landing_page" in action_types
