"""
W2 PR-R2 — AOL → alert_rule_catalog Confidence EWMA Writeback 測試
====================================================================
ADR-091 Task T2 飛輪斷鏈 C2 修復

測試範圍:
  - test_ewma_calculation              EWMA 公式正確（有舊值 / 無舊值兩路）
  - test_low_success_triggers_draft    低成功率且樣本 >= 5 → review_status='draft'
  - test_min_sample_threshold          樣本 < 5 不降 review_status
  - test_dry_run_no_db_write           feature flag=False → 不碰 DB
  - test_feature_flag_disabled_skips   flag=False 回傳 skipped=True
  - test_hermes_picks_up_draft         Hermes _fetch_noisy_rules SQL 含 OR review_status='draft'

禁止 Mock 測試鐵律:
  DB 依賴用 AsyncMock patch（get_db_context），只測業務邏輯分支。
  EWMA / sample 判斷為純 Python 邏輯，直接呼叫私有函式驗證。

建立: 2026-04-28 (台北時區) Claude Sonnet 4.6 (W2 PR-R2 ADR-091 T2)
"""
from __future__ import annotations

import os
from contextlib import asynccontextmanager
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

# conftest 前先設環境變數
os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")


# =============================================================================
# Helper: DB context mock
# =============================================================================

def _make_db_ctx(fetch_one_return=None, execute_side_effect=None):
    """
    回傳 (ctx_factory, mock_db)。
    simulate get_db_context() 的 async context manager。
    """
    mock_db = AsyncMock()

    if execute_side_effect is not None:
        mock_db.execute = AsyncMock(side_effect=execute_side_effect)

    if fetch_one_return is not None:
        mock_result = MagicMock()
        mock_result.one_or_none.return_value = fetch_one_return
        mock_db.execute = AsyncMock(return_value=mock_result)

    @asynccontextmanager
    async def _ctx():
        yield mock_db

    return _ctx, mock_db


def _catalog_row(confidence=None, review_status=None):
    """建構 alert_rule_catalog 假 row。"""
    row = MagicMock()
    row.rule_id = 1
    row.confidence = confidence
    row.review_status = review_status
    return row


# =============================================================================
# test_ewma_calculation — EWMA 計算正確性
# =============================================================================

@pytest.mark.asyncio
async def test_ewma_calculation_with_existing_confidence():
    """
    有舊 confidence 時：new = 0.7 * old + 0.3 * recent_sr
    """
    from src.jobs.aol_to_catalog_writeback_job import _update_catalog_confidence

    old_conf = 0.80
    recent_sr = 0.50
    expected = round(0.7 * old_conf + 0.3 * recent_sr, 2)  # 0.71

    # mock: 第一次 execute 回傳 existing row，第二次 execute 為 UPDATE
    existing_row = _catalog_row(confidence=old_conf, review_status="approved")
    call_count = 0

    @asynccontextmanager
    async def _ctx():
        mock_db = AsyncMock()

        async def _execute(sql, params=None):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                # SELECT 查詢
                r = MagicMock()
                r.one_or_none.return_value = existing_row
                return r
            else:
                # UPDATE
                return MagicMock()

        mock_db.execute = _execute
        yield mock_db

    sample = {
        "alertname": "HostHighCpuLoad",
        "ok": 5,
        "total": 10,
        "recent_success_rate": recent_sr,
    }

    # lazy import: aol_to_catalog_writeback_job 內 from src.db.base import get_db_context
    # patch 源頭模組即可
    with patch("src.db.base.get_db_context", _ctx):
        updated, flagged = await _update_catalog_confidence(sample)

    assert updated is True
    assert flagged is False
    # call_count=2: SELECT + UPDATE（不降級）


@pytest.mark.asyncio
async def test_ewma_calculation_without_existing_confidence():
    """
    confidence IS NULL 時：new_confidence = recent_success_rate（初始化）
    """
    from src.jobs.aol_to_catalog_writeback_job import _update_catalog_confidence

    recent_sr = 0.75
    existing_row = _catalog_row(confidence=None, review_status=None)
    call_count = 0

    @asynccontextmanager
    async def _ctx():
        mock_db = AsyncMock()

        async def _execute(sql, params=None):
            nonlocal call_count
            call_count += 1
            r = MagicMock()
            r.one_or_none.return_value = existing_row
            return r

        mock_db.execute = _execute
        yield mock_db

    sample = {
        "alertname": "HostDiskFull",
        "ok": 6,
        "total": 8,
        "recent_success_rate": recent_sr,
    }

    with patch("src.db.base.get_db_context", _ctx):
        updated, flagged = await _update_catalog_confidence(sample)

    assert updated is True
    # 初始值 = recent_sr，不是低成功率 → 不降 draft
    assert flagged is False


# =============================================================================
# test_low_success_triggers_draft
# =============================================================================

@pytest.mark.asyncio
async def test_low_success_triggers_draft():
    """
    recent_success_rate < 0.3 且 total >= 5 → review_status 設為 'draft'，
    且 updated=True, flagged=True.
    """
    from src.jobs.aol_to_catalog_writeback_job import _update_catalog_confidence

    existing_row = _catalog_row(confidence=0.60, review_status="approved")
    updates_seen = []
    call_count = 0

    @asynccontextmanager
    async def _ctx():
        mock_db = AsyncMock()

        async def _execute(sql, params=None):
            nonlocal call_count
            call_count += 1
            r = MagicMock()
            r.one_or_none.return_value = existing_row
            if params:
                updates_seen.append(params)
            return r

        mock_db.execute = _execute
        yield mock_db

    sample = {
        "alertname": "KubeDeploymentReplicasMismatch",
        "ok": 1,
        "total": 10,   # >= 5
        "recent_success_rate": 0.10,   # < 0.3 → 觸發 draft
    }

    with patch("src.db.base.get_db_context", _ctx):
        updated, flagged = await _update_catalog_confidence(sample)

    assert updated is True
    assert flagged is True

    # 確認 UPDATE 帶了 review_status='draft'
    draft_update = next(
        (p for p in updates_seen if p.get("rs") == "draft"),
        None,
    )
    assert draft_update is not None, "應有帶 rs='draft' 的 UPDATE 參數"


# =============================================================================
# test_min_sample_threshold
# =============================================================================

@pytest.mark.asyncio
async def test_min_sample_threshold_no_flag():
    """
    recent_success_rate < 0.3 但 total < 5 → 不降 draft，只更新 confidence.
    """
    from src.jobs.aol_to_catalog_writeback_job import _update_catalog_confidence

    existing_row = _catalog_row(confidence=0.60, review_status="approved")
    updates_seen = []
    call_count = 0

    @asynccontextmanager
    async def _ctx():
        mock_db = AsyncMock()

        async def _execute(sql, params=None):
            nonlocal call_count
            call_count += 1
            r = MagicMock()
            r.one_or_none.return_value = existing_row
            if params:
                updates_seen.append(params)
            return r

        mock_db.execute = _execute
        yield mock_db

    sample = {
        "alertname": "SomeRareAlert",
        "ok": 0,
        "total": 3,     # < 5 → 不降
        "recent_success_rate": 0.0,
    }

    with patch("src.db.base.get_db_context", _ctx):
        updated, flagged = await _update_catalog_confidence(sample)

    assert updated is True
    assert flagged is False
    # 確認沒有帶 rs='draft' 的 UPDATE
    draft_update = next(
        (p for p in updates_seen if p.get("rs") == "draft"),
        None,
    )
    assert draft_update is None, "sample < 5 不應降 review_status"


# =============================================================================
# test_dry_run_no_db_write / test_feature_flag_disabled_skips
# =============================================================================

@pytest.mark.asyncio
async def test_feature_flag_disabled_skips():
    """
    ENABLE_AOL_WRITEBACK_JOB=False → run_aol_writeback_once 回傳 skipped=True，
    且不觸發任何 DB 操作。
    """
    from src.jobs.aol_to_catalog_writeback_job import run_aol_writeback_once

    db_call_count = 0

    @asynccontextmanager
    async def _ctx():
        nonlocal db_call_count
        db_call_count += 1
        yield AsyncMock()

    with patch("src.core.config.settings") as mock_settings:
        mock_settings.ENABLE_AOL_WRITEBACK_JOB = False

        # patch job module's settings reference
        with patch("src.jobs.aol_to_catalog_writeback_job.settings", mock_settings):
            result = await run_aol_writeback_once()

    assert result["skipped"] is True
    assert result["rules_sampled"] == 0
    assert result["rules_updated"] == 0
    assert result["rules_flagged_draft"] == 0
    assert db_call_count == 0, "feature flag=False 時不應碰 DB"


@pytest.mark.asyncio
async def test_dry_run_no_db_write():
    """
    同上：flag=False 時完全不寫 DB（別名測試，語義明確）.
    """
    from src.jobs.aol_to_catalog_writeback_job import run_aol_writeback_once

    written = []

    @asynccontextmanager
    async def _ctx():
        mock_db = AsyncMock()
        mock_db.execute = AsyncMock(side_effect=lambda *a, **kw: written.append(a))
        yield mock_db

    with patch("src.jobs.aol_to_catalog_writeback_job.settings") as mock_settings:
        mock_settings.ENABLE_AOL_WRITEBACK_JOB = False
        result = await run_aol_writeback_once()

    assert result["skipped"] is True
    assert len(written) == 0


# =============================================================================
# test_hermes_picks_up_draft — Hermes SQL 包含 OR review_status='draft' 條件
# =============================================================================

def test_hermes_sql_includes_draft_condition():
    """
    驗證 hermes_rule_quality_job._fetch_noisy_rules 的 SQL 包含 OR review_status = 'draft'
    （靜態檢查，不跑真實 DB）.

    W2 PR-R2 要求：Hermes 必須撈到 AOL writeback 標記的 draft rules。
    """
    import inspect
    from src.jobs import hermes_rule_quality_job

    # 讀取 _fetch_noisy_rules 的原始碼
    src = inspect.getsource(hermes_rule_quality_job._fetch_noisy_rules)

    assert "review_status = 'draft'" in src, (
        "Hermes _fetch_noisy_rules 缺少 OR review_status = 'draft' 條件 "
        "（W2 PR-R2 斷鏈 C2 修復要求此條件觸發 AOL writeback advisory）"
    )


@pytest.mark.asyncio
async def test_hermes_picks_up_needs_review_rules():
    """
    Hermes _fetch_noisy_rules 被呼叫時，若 DB 有 review_status='draft' 的 rule，
    應正常回傳（不因額外 OR 條件報錯）.
    """
    from src.jobs.hermes_rule_quality_job import _fetch_noisy_rules

    draft_row = MagicMock()
    draft_row.rule_id = 99
    draft_row.rule_name = "LowSuccessRate"
    draft_row.severity = "warning"
    draft_row.true_positive_count = 1
    draft_row.false_positive_count = 9
    draft_row.noise_rate = 0.9
    draft_row.last_fired_at = None
    draft_row.review_status = "draft"

    mock_result = MagicMock()
    mock_result.fetchall.return_value = [draft_row]

    @asynccontextmanager
    async def _ctx():
        mock_db = AsyncMock()
        mock_db.execute = AsyncMock(return_value=mock_result)
        yield mock_db

    with patch("src.db.base.get_db_context", _ctx):
        rules = await _fetch_noisy_rules()

    assert len(rules) == 1
    assert rules[0]["rule_name"] == "LowSuccessRate"
    assert rules[0]["review_status"] == "draft"
