"""Block B T0 — `StepRunner._get_step_run()` dict shape contract.

Plan v2.1.3 / spec V5 S7: tuple → dict shape 변경.
- atomic claim / stale steal / verify path 가 run_id + started_at +
  recovery_count 의존 — tuple index 오해석 위험을 0으로 만든다.
- caller 모두 명시 키 access 만 허용. tuple-index 접근은 grep guard 로 차단.

이 모듈은 _get_step_run() 의 반환 형식만 검증한다 (resume / verify /
ResumeDecision 등 후속 task 의 동작은 별도 task 에서 검증).
"""
from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import MagicMock

import pytest


REQUIRED_KEYS = {
    "status",
    "run_id",
    "started_at",
    "completed_count",
    "applicable_count",
    "recovery_count",
    "updated_at",
    # 010_step_lock_ownership — 락이 죽음을 경과 시간으로 추측하지 않고
    # 확인하려면 이 다섯 칸이 판정에 들어와야 한다.
    "owner_host",
    "owner_pid",
    "owner_boot_id",
    "heartbeat_at",
    "cancel_requested_at",
}

# 신원 칸이 전부 비어 있는 「구 행」 — 각 테스트가 필요한 칸만 덮어쓴다.
_LEGACY_OWNER_FIELDS = dict(
    owner_host=None,
    owner_pid=None,
    owner_boot_id=None,
    heartbeat_at=None,
    cancel_requested_at=None,
)


def _make_runner_with_row(row):
    """fetchone() 를 row 로 mock 한 StepRunner 인스턴스."""
    from app.core.step_runner import StepRunner

    runner = StepRunner.__new__(StepRunner)
    runner.project_id = "p-test"
    runner.episode_id = "e-test"
    runner.step_id = "text_cleanup"
    runner.db = MagicMock()
    runner.db.execute.return_value.fetchone.return_value = row
    return runner


class TestGetStepRunDictShape:
    def test_returns_dict_with_all_required_keys(self):
        """row 가 있으면 dict 반환, 모든 필수 키 포함."""
        row = SimpleNamespace(
            **_LEGACY_OWNER_FIELDS,
            status="completed",
            run_id="r-1",
            started_at="2026-05-08T00:00:00",
            completed_count=5,
            applicable_count=10,
            recovery_count=0,
            updated_at="2026-05-08T00:01:00",
        )
        runner = _make_runner_with_row(row)

        result = runner._get_step_run("text_cleanup")

        assert isinstance(result, dict), (
            "_get_step_run 은 dict 를 반환해야 함 — tuple 반환은 plan v2.1.3 / spec V5 S7 위반"
        )
        assert set(result.keys()) >= REQUIRED_KEYS, (
            f"필수 키 누락: {REQUIRED_KEYS - set(result.keys())}"
        )

    def test_returns_none_when_no_row(self):
        """fetchone() None → 반환 None (caller 가 first-run 인식)."""
        runner = _make_runner_with_row(None)

        result = runner._get_step_run("text_cleanup")

        assert result is None

    def test_dict_values_match_row_columns(self):
        """dict 값이 row 컬럼과 1:1 매칭 (변환 누락/혼동 차단)."""
        row = SimpleNamespace(
            **_LEGACY_OWNER_FIELDS,
            status="running",
            run_id="r-42",
            started_at="2026-05-08T01:23:45",
            completed_count=3,
            applicable_count=7,
            recovery_count=2,
            updated_at="2026-05-08T01:23:50",
        )
        runner = _make_runner_with_row(row)

        result = runner._get_step_run("text_cleanup")

        assert result["status"] == "running"
        assert result["run_id"] == "r-42"
        assert result["started_at"] == "2026-05-08T01:23:45"
        assert result["completed_count"] == 3
        assert result["applicable_count"] == 7
        assert result["recovery_count"] == 2
        assert result["updated_at"] == "2026-05-08T01:23:50"

    def test_recovery_count_null_coalesced_to_zero(self):
        """SQL COALESCE(recovery_count, 0) — legacy row (NULL recovery_count) 도 0 으로."""
        # COALESCE 가 SQL 단에서 적용되므로 mock row 는 이미 0 으로 수신
        row = SimpleNamespace(
            **_LEGACY_OWNER_FIELDS,
            status="failed",
            run_id="r-legacy",
            started_at=None,
            completed_count=0,
            applicable_count=5,
            recovery_count=0,
            updated_at="2026-05-08T02:00:00",
        )
        runner = _make_runner_with_row(row)

        result = runner._get_step_run("text_cleanup")

        assert result["recovery_count"] == 0
        assert result["started_at"] is None  # legacy: started_at NULL 도 dict 키에 보존

    def test_no_tuple_indexing_supported_on_result(self):
        """반환값은 dict — tuple 처럼 index 접근 시 KeyError(int) 또는 TypeError.

        plan v2.1.3 BLOCKING #1: tuple fallback 절대 도입 X.
        caller 가 실수로 result[0] 사용 시 즉시 fail 하도록.
        """
        row = SimpleNamespace(
            **_LEGACY_OWNER_FIELDS,
            status="completed",
            run_id="r-1",
            started_at="2026-05-08T00:00:00",
            completed_count=1,
            applicable_count=1,
            recovery_count=0,
            updated_at="2026-05-08T00:01:00",
        )
        runner = _make_runner_with_row(row)

        result = runner._get_step_run("text_cleanup")

        # dict 는 정수 key 미보유 → KeyError. tuple subclass 등으로 양쪽 호환을
        # 시도하면 본 assert 가 fail 한다 (silent backward-compat shim 차단).
        with pytest.raises((KeyError, TypeError)):
            _ = result[0]
