import sys

sys.path.insert(0, "/home/jay/workspace/teams/shared")

import json
import time

import yaml
from qc import scenario_runner

# ── 헬퍼 ────────────────────────────────────────────────────────────────────


def _write_yaml(path, data):
    with open(path, "w", encoding="utf-8") as f:
        yaml.dump(data, f, allow_unicode=True)


def _write_json(path, data):
    with open(path, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False)


# ── 테스트 케이스 ─────────────────────────────────────────────────────────────


def test_run_scenarios_all_pass(tmp_path):
    """subprocess 타입 시나리오가 모두 통과하면 gate='PASS' 반환"""
    scenario = [
        {
            "id": "SC-TEST-001",
            "category": "smoke",
            "target": ["dummy.py"],
            "type": "subprocess",
            "steps": [
                {
                    "action": "echo hello",
                    "expect_contains": "hello",
                }
            ],
            "priority": "must",
            "automatable": True,
            "description": "echo hello 테스트",
        }
    ]
    yaml_file = tmp_path / "test_pass.yaml"
    _write_yaml(str(yaml_file), scenario)

    result = scenario_runner.run_scenarios(scenarios_dir=str(tmp_path))

    assert result["gate"] == "PASS", f"gate가 PASS여야 하지만 {result['gate']} 반환됨"


def test_must_fail_triggers_gate_fail(tmp_path):
    """must 시나리오 1개 FAIL → gate='FAIL'이고 failures에 1건 이상 기록됨"""
    scenario = [
        {
            "id": "SC-TEST-002",
            "category": "smoke",
            "target": ["dummy.py"],
            "type": "subprocess",
            "steps": [
                {
                    "action": "echo hello",
                    "expect_contains": "XYZZY_IMPOSSIBLE_STRING_12345",
                }
            ],
            "priority": "must",
            "automatable": True,
            "description": "절대 통과하지 않는 expect_contains 테스트",
        }
    ]
    yaml_file = tmp_path / "test_fail.yaml"
    _write_yaml(str(yaml_file), scenario)

    result = scenario_runner.run_scenarios(scenarios_dir=str(tmp_path))

    assert result["gate"] == "FAIL", f"gate가 FAIL이어야 하지만 {result['gate']} 반환됨"
    assert len(result["failures"]) >= 1, "failures 목록에 1건 이상 있어야 함"


def test_empty_scenarios_level3_fail(tmp_path):
    """시나리오 0건 + level=3 → verify() 반환 status='FAIL'"""
    empty_dir = tmp_path / "empty"
    empty_dir.mkdir()

    result = scenario_runner.verify(
        task_id="test",
        scenarios_dir=str(empty_dir),
        level=3,
    )

    assert result["status"] == "FAIL", f"level=3에서 시나리오 없으면 FAIL이어야 하지만 {result['status']} 반환됨"
    details_str = str(result.get("details", ""))
    assert (
        "Lv.3" in details_str or "시나리오 없음" in details_str
    ), f"details에 'Lv.3' 또는 '시나리오 없음' 포함 필요: {details_str}"


def test_empty_scenarios_level1_skip(tmp_path):
    """시나리오 0건 + level=1 → verify() 반환 status='SKIP'"""
    empty_dir = tmp_path / "empty"
    empty_dir.mkdir()

    result = scenario_runner.verify(
        task_id="test",
        scenarios_dir=str(empty_dir),
        level=1,
    )

    assert result["status"] == "SKIP", f"level=1에서 시나리오 없으면 SKIP이어야 하지만 {result['status']} 반환됨"


def test_impact_filtering(tmp_path):
    """impact.json으로 시나리오 필터링 — affected에 없는 target은 제외됨"""
    scenarios = [
        {
            "id": "SC-FILTER-001",
            "category": "smoke",
            "target": ["a.py"],
            "type": "subprocess",
            "steps": [{"action": "echo a", "expect_contains": "a"}],
            "priority": "must",
            "automatable": True,
            "description": "a.py 시나리오",
        },
        {
            "id": "SC-FILTER-002",
            "category": "smoke",
            "target": ["b.py"],
            "type": "subprocess",
            "steps": [{"action": "echo b", "expect_contains": "b"}],
            "priority": "must",
            "automatable": True,
            "description": "b.py 시나리오 — 필터됨",
        },
    ]
    yaml_file = tmp_path / "filter_test.yaml"
    _write_yaml(str(yaml_file), scenarios)

    impact_data = {"affected": ["a.py"]}
    impact_file = tmp_path / "impact.json"
    _write_json(str(impact_file), impact_data)

    result = scenario_runner.run_scenarios(
        scenarios_dir=str(tmp_path),
        impact_file=str(impact_file),
    )

    assert result["total"] == 1, f"impact 필터 후 total=1이어야 하지만 {result['total']} 반환됨"


def test_execution_within_timeout(tmp_path):
    """단순 시나리오 실행이 30초 이내에 완료됨"""
    scenario = [
        {
            "id": "SC-TIMEOUT-001",
            "category": "smoke",
            "target": ["dummy.py"],
            "type": "subprocess",
            "steps": [{"action": "echo timing", "expect_contains": "timing"}],
            "priority": "must",
            "automatable": True,
            "description": "실행 시간 측정용",
        }
    ]
    yaml_file = tmp_path / "timeout_test.yaml"
    _write_yaml(str(yaml_file), scenario)

    start = time.monotonic()
    result = scenario_runner.run_scenarios(scenarios_dir=str(tmp_path))
    elapsed = time.monotonic() - start

    duration = result.get("duration_seconds", elapsed)
    assert duration < 30, f"실행 시간이 30초 초과: {duration:.2f}초"


# ── Playwright 타입 관련 테스트 ─────────────────────────────────────────────────


class MockLocator:
    def __init__(self, visible=True):
        self._visible = visible

    def is_visible(self):
        return self._visible

    def bounding_box(self):
        return {"x": 100, "y": 200, "width": 300, "height": 50} if self._visible else None


class MockPage:
    def __init__(self):
        self._visible = True
        self._screenshot_path = None

    def goto(self, _url, **_kwargs):
        pass

    def wait_for_load_state(self, _state):
        pass

    def fill(self, _selector, _value):
        pass

    def click(self, _selector):
        pass

    def is_visible(self, _selector):
        return self._visible

    def locator(self, _selector):
        return MockLocator(self._visible)

    def screenshot(self, path=None):
        self._screenshot_path = path

    def wait_for_timeout(self, _ms):
        pass


class MockContext:
    def __init__(self, page):
        self._page = page

    def new_page(self):
        return self._page

    def close(self):
        pass


class MockBrowser:
    def __init__(self, context):
        self._context = context

    def new_context(self, **kwargs):
        return self._context

    def close(self):
        pass


class MockPlaywright:
    def __init__(self, browser):
        self.chromium = type("Chromium", (), {"launch": lambda self, **kw: browser})()


def _make_playwright_mock(visible=True):
    """playwright mock 객체 생성 헬퍼."""
    page = MockPage()
    page._visible = visible
    context = MockContext(page)
    browser = MockBrowser(context)
    pw = MockPlaywright(browser)
    return pw, page


def test_playwright_scenario_skipped_when_not_automatable(tmp_path):
    """playwright 타입이지만 automatable=false → skipped"""
    scenario = [
        {
            "id": "SC-PW-001",
            "category": "e2e",
            "target": ["frontend/app.tsx"],
            "type": "playwright",
            "steps": [
                {"action": "navigate http://localhost:3000"},
                {"action": "assert_visible h1"},
            ],
            "priority": "must",
            "automatable": False,
            "description": "playwright automatable=false 테스트",
        }
    ]
    yaml_file = tmp_path / "pw_skip.yaml"
    _write_yaml(str(yaml_file), scenario)

    result = scenario_runner.run_scenarios(scenarios_dir=str(tmp_path))

    assert result["skipped"] == 1, f"skipped=1이어야 하지만 {result['skipped']} 반환됨"
    assert result["gate"] == "PASS", f"gate가 PASS여야 하지만 {result['gate']} 반환됨"


def test_playwright_scenario_pass(tmp_path, monkeypatch):
    """playwright 타입 시나리오가 정상 실행되면 passed"""
    scenario = [
        {
            "id": "SC-PW-002",
            "category": "e2e",
            "target": ["frontend/app.tsx"],
            "type": "playwright",
            "steps": [
                {"action": "navigate http://localhost:3000"},
                {"action": "assert_visible h1"},
            ],
            "priority": "must",
            "automatable": True,
            "description": "playwright 정상 실행 테스트",
        }
    ]
    yaml_file = tmp_path / "pw_pass.yaml"
    _write_yaml(str(yaml_file), scenario)

    pw_mock, page = _make_playwright_mock(visible=True)

    import contextlib

    @contextlib.contextmanager
    def mock_sync_playwright():
        yield pw_mock

    monkeypatch.setattr(
        "qc.scenario_runner.sync_playwright",
        mock_sync_playwright,
        raising=False,
    )

    result = scenario_runner.run_scenarios(scenarios_dir=str(tmp_path))

    assert result["passed"] == 1, f"passed=1이어야 하지만 {result['passed']} 반환됨"
    assert result["gate"] == "PASS", f"gate가 PASS여야 하지만 {result['gate']} 반환됨"


def test_playwright_scenario_assert_fail_returns_failed(tmp_path, monkeypatch):
    """playwright assert 실패 시 failed + reason에 스크린샷 경로 포함"""
    scenario = [
        {
            "id": "SC-PW-003",
            "category": "e2e",
            "target": ["frontend/app.tsx"],
            "type": "playwright",
            "steps": [
                {"action": "navigate http://localhost:3000"},
                {"action": "assert_visible .non-existent-element"},
            ],
            "priority": "must",
            "automatable": True,
            "description": "playwright assert 실패 테스트",
        }
    ]
    yaml_file = tmp_path / "pw_fail.yaml"
    _write_yaml(str(yaml_file), scenario)

    pw_mock, page = _make_playwright_mock(visible=False)

    import contextlib

    @contextlib.contextmanager
    def mock_sync_playwright():
        yield pw_mock

    monkeypatch.setattr(
        "qc.scenario_runner.sync_playwright",
        mock_sync_playwright,
        raising=False,
    )

    result = scenario_runner.run_scenarios(scenarios_dir=str(tmp_path))

    assert result["failed"] == 1, f"failed=1이어야 하지만 {result['failed']} 반환됨"
    assert result["gate"] == "FAIL", f"gate가 FAIL이어야 하지만 {result['gate']} 반환됨"
    assert len(result["failures"]) >= 1, "failures에 1건 이상 있어야 함"
    # reason 또는 screenshot 경로 포함 확인
    failure = result["failures"][0]
    assert "SC-PW-003" == failure["id"], f"실패 시나리오 ID가 SC-PW-003이어야 함: {failure}"


def test_verify_level3_requires_playwright(tmp_path):
    """level=3에서 playwright 시나리오가 없으면 → WARN"""
    # playwright 없이 subprocess만 있는 시나리오
    scenario = [
        {
            "id": "SC-PW-GATE-001",
            "category": "smoke",
            "target": ["backend/api.py"],
            "type": "subprocess",
            "steps": [{"action": "echo ok", "expect_contains": "ok"}],
            "priority": "must",
            "automatable": True,
            "description": "playwright 없는 Lv.3 시나리오",
        }
    ]
    yaml_file = tmp_path / "no_pw_level3.yaml"
    _write_yaml(str(yaml_file), scenario)

    result = scenario_runner.verify(
        task_id="test-lv3",
        scenarios_dir=str(tmp_path),
        level=3,
    )

    assert result["status"] == "WARN", f"level=3에서 playwright 없으면 WARN이어야 하지만 {result['status']} 반환됨"
    details_str = str(result.get("details", ""))
    assert "playwright" in details_str.lower(), f"details에 'playwright' 포함 필요: {details_str}"


def test_resolve_placeholders(monkeypatch):
    """환경변수 기반 플레이스홀더 치환 동작 확인"""
    monkeypatch.setenv("SCENARIO_test_doc_id", "doc-123")
    result = scenario_runner._resolve_placeholders("http://localhost:3000/docs/{test_doc_id}")
    assert result == "http://localhost:3000/docs/doc-123"

    # 환경변수 없는 플레이스홀더는 원본 유지
    result2 = scenario_runner._resolve_placeholders("{unknown_var}")
    assert result2 == "{unknown_var}"


def test_verify_level2_skips_playwright_gate(tmp_path):
    """level=2에서 playwright 시나리오가 없어도 → 기존 동작 유지"""
    scenario = [
        {
            "id": "SC-PW-GATE-002",
            "category": "smoke",
            "target": ["backend/api.py"],
            "type": "subprocess",
            "steps": [{"action": "echo ok", "expect_contains": "ok"}],
            "priority": "must",
            "automatable": True,
            "description": "playwright 없는 Lv.2 시나리오",
        }
    ]
    yaml_file = tmp_path / "no_pw_level2.yaml"
    _write_yaml(str(yaml_file), scenario)

    result = scenario_runner.verify(
        task_id="test-lv2",
        scenarios_dir=str(tmp_path),
        level=2,
    )

    # level=2에서는 playwright 게이트 없이 기존 PASS 동작
    assert result["status"] == "PASS", f"level=2에서는 playwright 없어도 PASS여야 하지만 {result['status']} 반환됨"


# ── 기능 1: Playwright 순차 실행 분리 ──────────────────────────────────────────


def test_playwright_runs_sequentially(tmp_path, monkeypatch):
    """playwright 시나리오가 병렬이 아닌 순차 실행되는지 확인.
    3개 playwright + 2개 subprocess 시나리오 → subprocess는 병렬, playwright는 순차."""
    import contextlib

    # 실행 순서 기록 리스트
    execution_order: list[str] = []

    pw_scenarios = [
        {
            "id": f"SC-SEQ-PW-{i:03d}",
            "category": "e2e",
            "target": ["frontend/app.tsx"],
            "type": "playwright",
            "steps": [{"action": "navigate http://localhost:3000"}],
            "priority": "must",
            "automatable": True,
            "description": f"playwright 순차 실행 테스트 {i}",
        }
        for i in range(1, 4)
    ]
    sp_scenarios = [
        {
            "id": f"SC-SEQ-SP-{i:03d}",
            "category": "smoke",
            "target": ["backend/api.py"],
            "type": "subprocess",
            "steps": [{"action": "echo ok", "expect_contains": "ok"}],
            "priority": "must",
            "automatable": True,
            "description": f"subprocess 병렬 실행 테스트 {i}",
        }
        for i in range(1, 3)
    ]

    yaml_file = tmp_path / "seq_test.yaml"
    _write_yaml(str(yaml_file), pw_scenarios + sp_scenarios)

    pw_mock, _ = _make_playwright_mock(visible=True)

    @contextlib.contextmanager
    def mock_sync_playwright():
        yield pw_mock

    monkeypatch.setattr("qc.scenario_runner.sync_playwright", mock_sync_playwright, raising=False)

    # _run_playwright_scenario를 패치해서 실행 순서 기록
    original_run_playwright = scenario_runner._run_playwright_scenario

    def recording_run_playwright(scenario: dict, storage_state: str = "") -> dict:
        execution_order.append(scenario["id"])
        return {"id": scenario["id"], "status": "passed", "reason": ""}

    monkeypatch.setattr("qc.scenario_runner._run_playwright_scenario", recording_run_playwright)

    result = scenario_runner.run_scenarios(scenarios_dir=str(tmp_path))

    # 결과 집계 확인
    assert result["passed"] == 5, f"passed=5여야 하지만 {result['passed']} 반환됨"
    assert result["failed"] == 0

    # playwright 3개가 순차 기록되었는지 확인
    pw_ids = [sc_id for sc_id in execution_order if "PW" in sc_id]
    assert len(pw_ids) == 3, f"playwright 3개 실행 기록이어야 하지만 {pw_ids}"

    # 순차 실행 검증: recording_run_playwright는 ThreadPoolExecutor 외부에서 호출되어야 함
    # (ThreadPoolExecutor 내에서 호출 시 순서가 보장되지 않음)
    # 여기서는 playwright ID들이 execution_order에 모두 포함되어 있는지 확인
    expected_pw_ids = {f"SC-SEQ-PW-{i:03d}" for i in range(1, 4)}
    assert set(pw_ids) == expected_pw_ids, f"playwright ID 불일치: {pw_ids}"


# ── 기능 2: TTL 체크 통합 ──────────────────────────────────────────────────────


def test_ttl_check_called_before_playwright(tmp_path, monkeypatch):
    """playwright 시나리오 실행 전 check_and_refresh_ttl 호출 확인"""
    import contextlib

    ttl_call_log: list[bool] = []

    scenario = [
        {
            "id": "SC-TTL-001",
            "category": "e2e",
            "target": ["frontend/app.tsx"],
            "type": "playwright",
            "steps": [{"action": "navigate http://localhost:3000"}],
            "priority": "must",
            "automatable": True,
            "description": "TTL 체크 테스트",
        }
    ]
    yaml_file = tmp_path / "ttl_test.yaml"
    _write_yaml(str(yaml_file), scenario)

    pw_mock, _ = _make_playwright_mock(visible=True)

    @contextlib.contextmanager
    def mock_sync_playwright():
        yield pw_mock

    monkeypatch.setattr("qc.scenario_runner.sync_playwright", mock_sync_playwright, raising=False)

    # check_and_refresh_ttl mock: 항상 False 반환 (갱신 불필요)
    def mock_check_and_refresh_ttl(*args, **kwargs) -> bool:
        ttl_call_log.append(True)
        return False

    # setup_auth 모듈 mock: lazy import를 intercept
    import types

    mock_setup_auth = types.ModuleType("qc.auth.setup_auth")
    mock_setup_auth.check_and_refresh_ttl = mock_check_and_refresh_ttl  # type: ignore[attr-defined]

    monkeypatch.setitem(sys.modules, "qc.auth.setup_auth", mock_setup_auth)

    result = scenario_runner.run_scenarios(scenarios_dir=str(tmp_path))

    assert result["passed"] == 1, f"passed=1이어야 하지만 {result['passed']} 반환됨"
    assert len(ttl_call_log) >= 1, "check_and_refresh_ttl이 최소 1회 호출되어야 함"


def test_ttl_check_warning_logged_when_expired(tmp_path, monkeypatch, capsys):
    """TTL 만료 시 경고 로그 출력 확인"""
    import contextlib
    import types

    scenario = [
        {
            "id": "SC-TTL-002",
            "category": "e2e",
            "target": ["frontend/app.tsx"],
            "type": "playwright",
            "steps": [{"action": "navigate http://localhost:3000"}],
            "priority": "must",
            "automatable": True,
            "description": "TTL 만료 경고 테스트",
        }
    ]
    yaml_file = tmp_path / "ttl_expire_test.yaml"
    _write_yaml(str(yaml_file), scenario)

    pw_mock, _ = _make_playwright_mock(visible=True)

    @contextlib.contextmanager
    def mock_sync_playwright():
        yield pw_mock

    monkeypatch.setattr("qc.scenario_runner.sync_playwright", mock_sync_playwright, raising=False)

    # check_and_refresh_ttl mock: True 반환 (갱신 필요)
    def mock_check_expired(*args, **kwargs) -> bool:
        return True

    mock_setup_auth = types.ModuleType("qc.auth.setup_auth")
    mock_setup_auth.check_and_refresh_ttl = mock_check_expired  # type: ignore[attr-defined]
    monkeypatch.setitem(sys.modules, "qc.auth.setup_auth", mock_setup_auth)

    scenario_runner.run_scenarios(scenarios_dir=str(tmp_path))

    captured = capsys.readouterr()
    assert (
        "TTL" in captured.out or "storageState" in captured.out
    ), f"TTL 만료 경고가 출력되어야 하지만 출력 없음. stdout: {captured.out!r}"


def test_ttl_not_called_without_playwright(tmp_path, monkeypatch):
    """playwright 시나리오 없으면 check_and_refresh_ttl 호출 안 됨"""
    import types

    ttl_call_log: list[bool] = []

    scenario = [
        {
            "id": "SC-TTL-003",
            "category": "smoke",
            "target": ["backend/api.py"],
            "type": "subprocess",
            "steps": [{"action": "echo ok", "expect_contains": "ok"}],
            "priority": "must",
            "automatable": True,
            "description": "subprocess only - TTL 호출 안됨",
        }
    ]
    yaml_file = tmp_path / "no_pw_ttl.yaml"
    _write_yaml(str(yaml_file), scenario)

    def mock_check_and_refresh_ttl(*args, **kwargs) -> bool:
        ttl_call_log.append(True)
        return False

    mock_setup_auth = types.ModuleType("qc.auth.setup_auth")
    mock_setup_auth.check_and_refresh_ttl = mock_check_and_refresh_ttl  # type: ignore[attr-defined]
    monkeypatch.setitem(sys.modules, "qc.auth.setup_auth", mock_setup_auth)

    result = scenario_runner.run_scenarios(scenarios_dir=str(tmp_path))

    assert result["passed"] == 1
    assert len(ttl_call_log) == 0, "playwright 없으면 TTL 체크 호출 안됨"


# ── 기능 3: --stats 옵션 + 중복 감지 ──────────────────────────────────────────


def test_show_stats_counts_by_project(tmp_path):
    """프로젝트별 시나리오 카운트 정상 동작"""
    # insuwiki 프로젝트 디렉토리
    insuwiki_dir = tmp_path / "insuwiki"
    insuwiki_dir.mkdir()
    insuwiki_scenarios = [
        {
            "id": f"SC-INSUWIKI-{i:03d}",
            "category": "smoke",
            "target": ["insuwiki/main.py"],
            "type": "subprocess",
            "steps": [{"action": "echo ok"}],
            "priority": "must",
            "automatable": True,
            "description": f"insuwiki 시나리오 {i}",
        }
        for i in range(1, 4)
    ]
    _write_yaml(str(insuwiki_dir / "insuwiki.yaml"), insuwiki_scenarios)

    # dashboard 프로젝트 디렉토리
    dashboard_dir = tmp_path / "dashboard"
    dashboard_dir.mkdir()
    dashboard_scenarios = [
        {
            "id": f"SC-DASH-{i:03d}",
            "category": "smoke",
            "target": ["dashboard/main.py"],
            "type": "subprocess",
            "steps": [{"action": "echo ok"}],
            "priority": "must",
            "automatable": True,
            "description": f"dashboard 시나리오 {i}",
        }
        for i in range(1, 3)
    ]
    _write_yaml(str(dashboard_dir / "dashboard.yaml"), dashboard_scenarios)

    result = scenario_runner.show_stats(scenarios_dir=str(tmp_path))

    assert "projects" in result, "결과에 'projects' 키가 있어야 함"
    assert "total" in result, "결과에 'total' 키가 있어야 함"
    assert "duplicates" in result, "결과에 'duplicates' 키가 있어야 함"
    assert result["projects"]["insuwiki"] == 3, f"insuwiki 카운트=3이어야 하지만 {result['projects'].get('insuwiki')}"
    assert (
        result["projects"]["dashboard"] == 2
    ), f"dashboard 카운트=2이어야 하지만 {result['projects'].get('dashboard')}"
    assert result["total"] == 5, f"total=5이어야 하지만 {result['total']}"


def test_detect_duplicates_finds_dupes(tmp_path):
    """중복 ID 감지"""
    scenarios = [
        {"id": "SC-DUP-001", "type": "subprocess", "steps": []},
        {"id": "SC-DUP-001", "type": "subprocess", "steps": []},  # 중복
        {"id": "SC-DUP-002", "type": "subprocess", "steps": []},
        {"id": "SC-DUP-003", "type": "subprocess", "steps": []},
        {"id": "SC-DUP-003", "type": "subprocess", "steps": []},  # 중복
    ]

    duplicates = scenario_runner._detect_duplicates(scenarios)

    assert "SC-DUP-001" in duplicates, f"SC-DUP-001이 중복 목록에 있어야 함: {duplicates}"
    assert "SC-DUP-003" in duplicates, f"SC-DUP-003이 중복 목록에 있어야 함: {duplicates}"
    assert "SC-DUP-002" not in duplicates, f"SC-DUP-002는 중복이 아님: {duplicates}"


def test_detect_duplicates_no_dupes(tmp_path):
    """중복 없으면 빈 리스트"""
    scenarios = [
        {"id": "SC-UNIQ-001", "type": "subprocess", "steps": []},
        {"id": "SC-UNIQ-002", "type": "subprocess", "steps": []},
        {"id": "SC-UNIQ-003", "type": "subprocess", "steps": []},
    ]

    duplicates = scenario_runner._detect_duplicates(scenarios)

    assert duplicates == [], f"중복 없으면 빈 리스트여야 하지만 {duplicates}"


# ── 기능 4: TTL 만료 시 자동 갱신 호출 ────────────────────────────────────────


def test_auto_refresh_called_when_ttl_expired(tmp_path, monkeypatch):
    """TTL 만료 시 _auto_refresh_storage_state 함수가 호출되는지 확인"""
    import contextlib
    import types

    scenario = [
        {
            "id": "SC-AUTOREFRESH-001",
            "category": "e2e",
            "target": ["frontend/app.tsx"],
            "type": "playwright",
            "steps": [{"action": "navigate http://localhost:3000"}],
            "priority": "must",
            "automatable": True,
            "description": "자동 갱신 호출 확인 테스트",
        }
    ]
    yaml_file = tmp_path / "auto_refresh_test.yaml"
    _write_yaml(str(yaml_file), scenario)

    pw_mock, _ = _make_playwright_mock(visible=True)

    @contextlib.contextmanager
    def mock_sync_playwright():
        yield pw_mock

    monkeypatch.setattr("qc.scenario_runner.sync_playwright", mock_sync_playwright, raising=False)

    # check_and_refresh_ttl: True 반환 (갱신 필요)
    def mock_check_expired(*args, **kwargs) -> bool:
        return True

    mock_setup_auth = types.ModuleType("qc.auth.setup_auth")
    mock_setup_auth.check_and_refresh_ttl = mock_check_expired  # type: ignore[attr-defined]
    monkeypatch.setitem(sys.modules, "qc.auth.setup_auth", mock_setup_auth)

    # _auto_refresh_storage_state 호출 여부 기록
    auto_refresh_call_log: list[bool] = []

    def mock_auto_refresh(storage_state_path: str = "") -> bool:
        auto_refresh_call_log.append(True)
        return True

    monkeypatch.setattr("qc.scenario_runner._auto_refresh_storage_state", mock_auto_refresh)

    scenario_runner.run_scenarios(scenarios_dir=str(tmp_path))

    assert len(auto_refresh_call_log) >= 1, "_auto_refresh_storage_state가 TTL 만료 시 최소 1회 호출되어야 함"


def test_auto_refresh_failure_logs_warning(tmp_path, monkeypatch, capsys):
    """자동 갱신 실패 시 경고 로그가 출력되고 playwright 시나리오가 계속 실행되는지 확인"""
    import contextlib
    import types

    scenario = [
        {
            "id": "SC-AUTOREFRESH-002",
            "category": "e2e",
            "target": ["frontend/app.tsx"],
            "type": "playwright",
            "steps": [{"action": "navigate http://localhost:3000"}],
            "priority": "must",
            "automatable": True,
            "description": "자동 갱신 실패 경고 테스트",
        }
    ]
    yaml_file = tmp_path / "auto_refresh_fail_test.yaml"
    _write_yaml(str(yaml_file), scenario)

    pw_mock, _ = _make_playwright_mock(visible=True)

    @contextlib.contextmanager
    def mock_sync_playwright():
        yield pw_mock

    monkeypatch.setattr("qc.scenario_runner.sync_playwright", mock_sync_playwright, raising=False)

    # check_and_refresh_ttl: True 반환 (갱신 필요)
    def mock_check_expired(*args, **kwargs) -> bool:
        return True

    mock_setup_auth = types.ModuleType("qc.auth.setup_auth")
    mock_setup_auth.check_and_refresh_ttl = mock_check_expired  # type: ignore[attr-defined]
    monkeypatch.setitem(sys.modules, "qc.auth.setup_auth", mock_setup_auth)

    # _auto_refresh_storage_state: 실패 반환 (False)
    def mock_auto_refresh_fail(storage_state_path: str = "") -> bool:
        return False

    monkeypatch.setattr("qc.scenario_runner._auto_refresh_storage_state", mock_auto_refresh_fail)

    result = scenario_runner.run_scenarios(scenarios_dir=str(tmp_path))

    captured = capsys.readouterr()
    # 실패 경고 메시지가 출력되어야 함
    assert (
        "실패" in captured.out or "수동" in captured.out
    ), f"자동 갱신 실패 경고가 출력되어야 하지만 stdout: {captured.out!r}"
    # playwright 시나리오는 계속 실행되어야 함 (passed 또는 failed, skipped 중 하나)
    assert result["total"] == 1, f"시나리오 1개가 실행되어야 하지만 total={result['total']}"
