#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：test_phase4.py
模块描述：阶段 4 单元测试（状态验证 + api_call + finally）

覆盖：
- _verify_step_state：verify 类型映射 / must 语义 / ENFORCE_VERIFICATION
- _run_finally_steps：finally 无条件执行 / on_failure 仅失败执行 / 失败不掩盖主结果
- _do_api_call：复用 ApiTestExecutor / ctx 渲染 / save_as JSONPath 写上下文 / 失败抛错

作者：czj
创建日期：2026-08-18
"""

from types import SimpleNamespace
from unittest.mock import MagicMock, patch

import pytest

from app.config import settings
from app.executors.playwright_executor import PlaywrightExecutor, StepResult
from app.executors.ui_context import ExecutionContext


def make_executor(**overrides) -> PlaywrightExecutor:
    """构造执行器（默认空配置，可覆盖 _do_assert 等）"""
    ex = PlaywrightExecutor({})
    for k, v in overrides.items():
        setattr(ex, k, v)
    return ex


class FakeCaseResult:
    """ApiCaseResult 鸭子类型（_do_api_call 只读 status/error/step）"""

    def __init__(self, status="passed", error="", response_body=""):
        self.status = status
        self.error = error
        self.step = SimpleNamespace(response_body=response_body)


class FakeApiExecutor:
    """ApiTestExecutor 鸭子类型（捕获 config + case，可预置结果）"""

    def __init__(self, config):
        self.config = config
        self.case = None
        self.result = FakeCaseResult()
        self.started = False
        self.stopped = False

    def start(self):
        self.started = True

    def stop(self):
        self.stopped = True

    def execute_case(self, case):
        self.case = case
        return self.result


# ==================== _verify_step_state ====================

class TestVerifyStepState:
    def test_no_verify_returns_true(self):
        ex = make_executor()
        ex._do_assert = MagicMock()
        assert ex._verify_step_state({"action": "click"}, {}, None) is True
        ex._do_assert.assert_not_called()

    def test_text_visible_pass(self):
        ex = make_executor()
        ex._do_assert = MagicMock()
        step = {"verify": {"type": "text_visible", "selector": ".tab", "text": "预定会议", "must": True}}
        assert ex._verify_step_state(step, {}, None) is True
        ex._do_assert.assert_called_once_with(
            {"type": "contains", "selectors": [".tab"], "expected": "预定会议"}, "预定会议"
        )

    def test_must_false_failure_only_warns(self):
        ex = make_executor()
        ex._do_assert = MagicMock(side_effect=AssertionError("not found"))
        step = {"verify": {"type": "text_visible", "selector": ".tab", "text": "预定会议", "must": False}}
        assert ex._verify_step_state(step, {}, None) is True

    def test_must_true_failure_raises(self):
        ex = make_executor()
        ex._do_assert = MagicMock(side_effect=AssertionError("not found"))
        step = {"verify": {"type": "text_visible", "selector": ".tab", "text": "预定会议", "must": True}}
        with pytest.raises(AssertionError):
            ex._verify_step_state(step, {}, None)

    def test_dialog_mapping(self):
        ex = make_executor()
        ex._do_assert = MagicMock()
        step = {"verify": {"type": "dialog", "text": "创建成功", "must": True}}
        assert ex._verify_step_state(step, {}, None) is True
        ap = ex._do_assert.call_args[0][0]
        assert ap["type"] == "element_exists"
        assert any(".el-dialog" in s for s in ap["selectors"])

    def test_selector_fallback_to_chain(self):
        """verify.selector 缺省时用语义链首选择器兜底"""
        ex = make_executor()
        ex._do_assert = MagicMock()
        step = {"verify": {"type": "text_visible", "text": "预定会议", "must": True}}
        assert ex._verify_step_state(step, {}, ".el-tabs__item:has-text(\"预定会议\")") is True
        ap = ex._do_assert.call_args[0][0]
        assert ap["selectors"] == ['.el-tabs__item:has-text("预定会议")']

    def test_url_contains(self):
        ex = make_executor()
        ex._do_assert = MagicMock()
        step = {"verify": {"type": "url_contains", "text": "meetingV3", "must": True}}
        assert ex._verify_step_state(step, {}, None) is True
        assert ex._do_assert.call_args[0][0] == {"type": "url_contains", "expected": "meetingV3"}

    def test_enforce_verification_gated(self):
        """ENFORCE_VERIFICATION=true 且 expected 非空 → 默认校验；false 时无 verify 直接过"""
        ex = make_executor()
        old = settings.ENFORCE_VERIFICATION
        try:
            # 关闭时不调默认校验
            settings.ENFORCE_VERIFICATION = False
            ex._do_assert = MagicMock()
            assert ex._verify_step_state({"action": "click", "expected": "创建成功"}, {}, None) is True
            ex._do_assert.assert_not_called()
            # 开启时调用默认校验
            settings.ENFORCE_VERIFICATION = True
            ex._do_assert = MagicMock()
            assert ex._verify_step_state({"action": "click", "expected": "创建成功"}, {}, None) is True
            assert ex._do_assert.called
        finally:
            settings.ENFORCE_VERIFICATION = old


# ==================== _run_finally_steps ====================

class TestRunFinallySteps:
    class FakeResult:
        def __init__(self, status="passed"):
            self.status = status
            self.steps_result = []
            self.error_message = None

    def _mk(self, status="passed"):
        ex = make_executor()
        result = self.FakeResult(status)
        return ex, result

    def test_finally_runs_on_success(self):
        ex, result = self._mk("passed")
        ex.execute_step = MagicMock(return_value=StepResult(order=99, status="passed"))
        steps = [
            {"order": 1, "name": "清理", "action": "api_call", "run_on": "finally"},
        ]
        ex._run_finally_steps(steps, result, None)
        ex.execute_step.assert_called_once()
        assert len(result.steps_result) == 1
        assert result.status == "passed"

    def test_on_failure_skipped_on_success(self):
        ex, result = self._mk("passed")
        ex.execute_step = MagicMock(return_value=StepResult(order=99, status="passed"))
        steps = [{"order": 1, "name": "补救", "action": "api_call", "run_on": "on_failure"}]
        ex._run_finally_steps(steps, result, None)
        ex.execute_step.assert_not_called()

    def test_on_failure_runs_on_failure(self):
        ex, result = self._mk("failed")
        ex.execute_step = MagicMock(return_value=StepResult(order=99, status="passed"))
        steps = [{"order": 1, "name": "补救", "action": "api_call", "run_on": "on_failure"}]
        ex._run_finally_steps(steps, result, None)
        ex.execute_step.assert_called_once()
        assert result.status == "failed"  # 不掩盖失败结果

    def test_always_steps_not_run_in_finally(self):
        ex, result = self._mk("passed")
        ex.execute_step = MagicMock(return_value=StepResult(order=99, status="passed"))
        steps = [{"order": 1, "name": "普通步骤", "action": "click", "run_on": "always"}]
        ex._run_finally_steps(steps, result, None)
        ex.execute_step.assert_not_called()

    def test_finally_failure_does_not_override_passed(self):
        ex, result = self._mk("passed")
        ex.execute_step = MagicMock(return_value=StepResult(order=99, status="failed", error="boom"))
        steps = [{"order": 1, "name": "清理", "action": "api_call", "run_on": "finally"}]
        ex._run_finally_steps(steps, result, None)
        assert result.status == "failed"
        assert "清理步骤失败" in (result.error_message or "")

    def test_finally_exception_annotates(self):
        ex, result = self._mk("passed")
        ex.execute_step = MagicMock(side_effect=RuntimeError("network"))
        steps = [{"order": 1, "name": "清理", "action": "api_call", "run_on": "finally"}]
        ex._run_finally_steps(steps, result, None)
        assert result.status == "failed"
        assert "清理步骤异常" in (result.error_message or "")


# ==================== _do_api_call ====================

class TestDoApiCall:
    def _mk(self, fake_result: FakeCaseResult):
        fake_cls = MagicMock(side_effect=lambda cfg: fake_result.executor)
        patcher = patch("app.executors.api_test_executor.ApiTestExecutor", fake_cls)
        ex = make_executor()
        ex._ctx = ExecutionContext({})
        ex._ctx.set("meetingName", "自动化-XYZ1")
        return ex, patcher

    def test_success_renders_and_saves(self):
        fake = FakeApiExecutor(None)
        fake.result = FakeCaseResult(status="passed", response_body='{"data": {"id": 7}}')
        ex = make_executor()
        ex._ctx = ExecutionContext({})
        ex._ctx.set("meetingName", "自动化-XYZ1")
        patcher = patch(
            "app.executors.api_test_executor.ApiTestExecutor",
            MagicMock(side_effect=lambda cfg: fake),
        )
        with patcher:
            result = ex._do_api_call({
                "api_case": {
                    "name": "查询会议",
                    "steps": {
                        "method": "POST",
                        "url": "/api/meeting/query",
                        "body": {"name": "{__CTX:meetingName__}"},
                    },
                },
                "save_as": {"meetingId": "$.data.id"},
            })
        assert result.status == "passed"
        assert fake.started and fake.stopped
        # ctx 渲染生效：body 已替换 CTX 变量
        body = fake.case["steps"]["body"]
        assert body["name"] == "自动化-XYZ1"
        # save_as JSONPath 写上下文
        assert ex._ctx.get("meetingId") == 7

    def test_failure_raises(self):
        fake = FakeApiExecutor(None)
        fake.result = FakeCaseResult(status="failed", error="timeout")
        ex = make_executor()
        patcher = patch(
            "app.executors.api_test_executor.ApiTestExecutor",
            MagicMock(side_effect=lambda cfg: fake),
        )
        with patcher, pytest.raises(Exception, match="api_call 失败"):
            ex._do_api_call({"api_case": {"name": "查询", "steps": {"method": "GET", "url": "/x"}}})
        assert fake.stopped

    def test_no_api_case_is_noop(self):
        fake = FakeApiExecutor(None)
        fake.result = FakeCaseResult(status="passed")
        ex = make_executor()
        patcher = patch(
            "app.executors.api_test_executor.ApiTestExecutor",
            MagicMock(side_effect=lambda cfg: fake),
        )
        with patcher:
            result = ex._do_api_call({})
        assert result.status == "passed"
