"""
회귀 테스트 — utils/critical_escalation_reporter (task-2513)

12건 필수 + 2건 보너스 (총 14건):
  - Critical 7 exact match (test_01)
  - Non-critical suppression (test_02)
  - Duplicate suppression (test_03)
  - EscalationPacket JSON round-trip (test_04)
  - Severity mapping (test_05)
  - format_packet_for_chair 4096자 제한 (test_06)
  - Audit JSONL 생성 검증 (test_07)
  - 회장 §9 critical fixture 6개 replay (test_08)
  - 회장 §10 auto-handled fixture 5개 replay (test_09)
  - FORBIDDEN_PATH_INTRUSION HIGH_CORE audit (test_10)
  - POST_MERGE_SMOKE_FAILED audit append (test_11)
  - STYLE_ONLY_GEMINI auto-handled audit (test_12)
  - Legacy 호환 7개 canonical 매핑 (test_13 - 보너스 G1)
  - Audit JSONL 다건 파싱 검증 (test_14 - 보너스)
"""
from __future__ import annotations

import dataclasses
import json
import sys
from datetime import datetime, timezone
from pathlib import Path

import pytest

# workspace root → sys.path (기존 regression 테스트 패턴 준수)
_WORKSPACE = Path(__file__).resolve().parent.parent.parent
_WORKSPACE_STR = str(_WORKSPACE)
# tests/conftest.py가 /home/jay/workspace를 sys.path에 삽입하여 이 worktree의 utils를
# 숨기는 문제를 방지하기 위해 worktree 경로를 맨 앞에 삽입한다.
if _WORKSPACE_STR in sys.path:
    sys.path.remove(_WORKSPACE_STR)
sys.path.insert(0, _WORKSPACE_STR)

from utils.automation_contracts import (  # noqa: E402  # pyright: ignore[reportMissingImports]
    CriticalEscalationType,
    EscalationPacket,
    RiskLevel,
)
from utils.critical_escalation_reporter import (  # noqa: E402  # pyright: ignore[reportMissingImports]
    LEGACY_CRITICAL_MAP,
    SEVERITY_MAP,
    format_packet_for_chair,
    is_duplicate,
    process_event,
)

# ---------------------------------------------------------------------------
# 공통 헬퍼
# ---------------------------------------------------------------------------

_CRITICAL_7 = [e.value for e in CriticalEscalationType]

_NOW = datetime(2026, 5, 9, 12, 0, 0, tzinfo=timezone.utc)


def _make_event(
    event_type: str,
    task_id: str = "task-2513",
    pr_number: int = 9001,
    evidence: dict | None = None,
) -> dict:
    """최소 필드를 갖춘 이벤트 dict 생성."""
    return {
        "task_id": task_id,
        "pr_number": pr_number,
        "event_type": event_type,
        "source": "test-suite",
        "evidence": evidence or {},
    }


def _global_audit_path(workspace_root: Path) -> Path:
    return workspace_root / "memory" / "orchestration-audit" / "critical-escalations.jsonl"


def _read_audit_lines(workspace_root: Path) -> list[dict]:
    p = _global_audit_path(workspace_root)
    if not p.exists():
        return []
    lines = [ln.strip() for ln in p.read_text(encoding="utf-8").splitlines() if ln.strip()]
    return [json.loads(ln) for ln in lines]


# ---------------------------------------------------------------------------
# test_01: Critical 7 — exact match
# ---------------------------------------------------------------------------

def test_01_critical_seven_exact_match(tmp_path: Path) -> None:
    """7개 CriticalEscalationType 값을 event_type으로 입력하면 모두 classification=='critical'이고
    escalation_type 이 enum value와 정확히 매칭되어야 한다."""
    for etype in _CRITICAL_7:
        result = process_event(
            _make_event(etype),
            workspace_root=tmp_path / etype,
            dry_run=True,
            now=_NOW,
        )
        assert result["classification"] == "critical", (
            f"{etype}: classification should be 'critical', got {result['classification']!r}"
        )
        assert result["escalation_type"] == etype, (
            f"{etype}: escalation_type mismatch. expected={etype!r}, got={result['escalation_type']!r}"
        )
        assert result["packet"] is not None, f"{etype}: packet should not be None"
        assert result["formatted_text"] is not None, f"{etype}: formatted_text should not be None"
        assert result["suppression_reason"] is None, (
            f"{etype}: suppression_reason should be None for critical"
        )


# ---------------------------------------------------------------------------
# test_02: Non-critical → auto-handled
# ---------------------------------------------------------------------------

def test_02_non_critical_suppression(tmp_path: Path) -> None:
    """Critical 7 외 이벤트는 classification=='auto-handled', packet/formatted_text is None,
    suppression_reason이 비어있지 않아야 한다."""
    non_critical_types = [
        "GEMINI_COMPLETED",
        "AUTO_MERGE_SUCCESS",
        "RANDOM_UNKNOWN_EVENT",
        "STYLE_ONLY_GEMINI",
        "FALSE_POSITIVE_GEMINI",
    ]
    for etype in non_critical_types:
        result = process_event(
            _make_event(etype),
            workspace_root=tmp_path / etype,
            dry_run=True,
            now=_NOW,
        )
        assert result["classification"] == "auto-handled", (
            f"{etype}: expected 'auto-handled', got {result['classification']!r}"
        )
        assert result["packet"] is None, f"{etype}: packet should be None"
        assert result["formatted_text"] is None, f"{etype}: formatted_text should be None"
        assert result["suppression_reason"], f"{etype}: suppression_reason should not be empty"
        assert result["escalation_type"] is None, f"{etype}: escalation_type should be None"


# ---------------------------------------------------------------------------
# test_03: Duplicate suppression
# ---------------------------------------------------------------------------

def test_03_duplicate_suppression(tmp_path: Path) -> None:
    """동일 (escalation_type, task_id, evidence_keys) 이벤트를 2회 처리하면
    첫 번째는 'critical', 두 번째는 'duplicate-suppressed' 여야 한다.
    audit JSONL 라인이 2개 생성되어야 한다."""
    ws = tmp_path / "dedup-ws"
    etype = "FORBIDDEN_PATH_INTRUSION"
    evidence = {"file": "utils/automation_contracts.py"}
    event = _make_event(etype, evidence=evidence)

    result1 = process_event(event, workspace_root=ws, dry_run=True, now=_NOW)
    assert result1["classification"] == "critical", (
        f"1st call should be 'critical', got {result1['classification']!r}"
    )

    result2 = process_event(event, workspace_root=ws, dry_run=True, now=_NOW)
    assert result2["classification"] == "duplicate-suppressed", (
        f"2nd call should be 'duplicate-suppressed', got {result2['classification']!r}"
    )
    assert result2["suppression_reason"], "suppression_reason should not be empty on duplicate"

    lines = _read_audit_lines(ws)
    assert len(lines) == 2, f"Expected 2 audit lines, got {len(lines)}"
    assert lines[0]["classification"] == "critical", "1st line should be 'critical'"
    assert lines[1]["classification"] == "duplicate-suppressed", "2nd line should be 'duplicate-suppressed'"

    # is_duplicate 함수 단위 검증: 같은 (type, task_id, evidence_keys) → True
    audit_log = ws / "memory" / "orchestration-audit" / "critical-escalations.jsonl"
    assert is_duplicate(
        CriticalEscalationType.FORBIDDEN_PATH_INTRUSION,
        event["task_id"],
        list(evidence.keys()),
        audit_log,
        window_sec=3600,
        now=_NOW,
    ), "is_duplicate must report True for repeated event within window"
    # 다른 task_id면 dedup 대상 아님
    assert not is_duplicate(
        CriticalEscalationType.FORBIDDEN_PATH_INTRUSION,
        "task-9999",
        list(evidence.keys()),
        audit_log,
        window_sec=3600,
        now=_NOW,
    ), "is_duplicate must report False for different task_id"


# ---------------------------------------------------------------------------
# test_04: EscalationPacket JSON round-trip
# ---------------------------------------------------------------------------

def test_04_packet_json_round_trip() -> None:
    """EscalationPacket을 dataclasses.asdict → json.dumps → json.loads 후
    모든 필드가 동일해야 한다."""
    packet = EscalationPacket(
        task_id="task-2513",
        pr_number=42,
        escalation_type=CriticalEscalationType.POST_MERGE_SMOKE_FAILED,
        reason="smoke test failed",
        why_auto_cannot_continue="manual intervention required",
        safe_options=["rollback", "hotfix"],
        recommended_option="rollback",
        evidence={"commit": "abc123", "test": "smoke"},
    )

    as_dict = dataclasses.asdict(packet)
    serialized = json.dumps(as_dict, ensure_ascii=False)
    restored = json.loads(serialized)

    assert restored["task_id"] == packet.task_id, "task_id mismatch"
    assert restored["pr_number"] == packet.pr_number, "pr_number mismatch"
    assert restored["escalation_type"] == packet.escalation_type.value, "escalation_type mismatch"
    assert restored["reason"] == packet.reason, "reason mismatch"
    assert restored["why_auto_cannot_continue"] == packet.why_auto_cannot_continue
    assert restored["safe_options"] == packet.safe_options, "safe_options mismatch"
    assert restored["recommended_option"] == packet.recommended_option
    assert restored["evidence"] == packet.evidence, "evidence mismatch"


# ---------------------------------------------------------------------------
# test_05: Severity mapping
# ---------------------------------------------------------------------------

def test_05_severity_mapping() -> None:
    """SEVERITY_MAP의 모든 7개 매핑이 RiskLevel.HIGH 또는 RiskLevel.HIGH_CORE 이어야 한다."""
    assert len(SEVERITY_MAP) == 7, f"Expected 7 entries in SEVERITY_MAP, got {len(SEVERITY_MAP)}"

    for escalation_type, risk_level in SEVERITY_MAP.items():
        assert isinstance(escalation_type, CriticalEscalationType), (
            f"Key {escalation_type!r} is not CriticalEscalationType"
        )
        assert risk_level in (RiskLevel.HIGH, RiskLevel.HIGH_CORE), (
            f"{escalation_type.value}: severity should be HIGH or HIGH_CORE, got {risk_level!r}"
        )

    # HIGH_CORE 확인
    high_core_expected = {
        CriticalEscalationType.FORBIDDEN_PATH_INTRUSION,
        CriticalEscalationType.REPLACEMENT_PR_AUTO_CREATION_FAILED_FOR_CONTAMINATED_DIFF,
        CriticalEscalationType.BLOCK_OVERRIDE_REQUIRED_OR_REASON_INSUFFICIENT,
        CriticalEscalationType.DEPENDENCY_CYCLE_OR_SERIAL_ONLY_COLLISION,
    }
    for et in high_core_expected:
        assert SEVERITY_MAP[et] == RiskLevel.HIGH_CORE, (
            f"{et.value} should be HIGH_CORE, got {SEVERITY_MAP[et]!r}"
        )

    # HIGH 확인
    high_expected = {
        CriticalEscalationType.GEMINI_REAL_BUG_REQUIRES_SCOPE_EXPANSION,
        CriticalEscalationType.REPLACEMENT_PR_FAILED,
        CriticalEscalationType.POST_MERGE_SMOKE_FAILED,
    }
    for et in high_expected:
        assert SEVERITY_MAP[et] == RiskLevel.HIGH, (
            f"{et.value} should be HIGH, got {SEVERITY_MAP[et]!r}"
        )


# ---------------------------------------------------------------------------
# test_06: format_packet_for_chair ≤ 4096자
# ---------------------------------------------------------------------------

def test_06_format_within_4096(tmp_path: Path) -> None:
    """모든 7개 critical type에 대해 evidence를 50KB로 채워도
    format_packet_for_chair 결과가 4096자 이하여야 한다."""
    large_evidence_str = "X" * 50_000
    for etype in CriticalEscalationType:
        result = process_event(
            _make_event(etype.value, evidence={"large_key": large_evidence_str}),
            workspace_root=tmp_path / etype.value,
            dry_run=True,
            now=_NOW,
        )
        assert result["classification"] == "critical", f"{etype.value}: should be critical"
        formatted = result["formatted_text"]
        assert formatted is not None, f"{etype.value}: formatted_text should not be None"
        assert len(formatted) <= 4096, (
            f"{etype.value}: formatted_text length {len(formatted)} exceeds 4096"
        )

        # format_packet_for_chair 직접 호출 검증 (custom max_len 적용)
        packet = result["packet"]
        assert packet is not None
        direct = format_packet_for_chair(packet, max_len=512)
        assert len(direct) <= 512, (
            f"{etype.value}: direct format_packet_for_chair(max_len=512) returned {len(direct)} chars"
        )


# ---------------------------------------------------------------------------
# test_07: Audit JSONL 생성 검증
# ---------------------------------------------------------------------------

def test_07_audit_jsonl_generation(tmp_path: Path) -> None:
    """Critical 1건 처리 후:
    - audit JSONL 파일에 라인 1개 존재
    - json.loads 성공
    - ts/task_id/escalation_type/classification/evidence_hash 키 존재
    - per-task 파일도 생성
    """
    ws = tmp_path / "audit-ws"
    task_id = "task-2513"
    etype = "DEPENDENCY_CYCLE_OR_SERIAL_ONLY_COLLISION"

    process_event(
        _make_event(etype, task_id=task_id),
        workspace_root=ws,
        dry_run=True,
        now=_NOW,
    )

    # 글로벌 JSONL
    global_path = _global_audit_path(ws)
    assert global_path.exists(), "Global audit JSONL file should exist"

    lines = [ln.strip() for ln in global_path.read_text(encoding="utf-8").splitlines() if ln.strip()]
    assert len(lines) == 1, f"Expected 1 audit line, got {len(lines)}"

    record = json.loads(lines[0])
    for required_key in ("ts", "task_id", "escalation_type", "classification", "evidence_hash"):
        assert required_key in record, f"Audit record missing key: {required_key!r}"

    assert record["task_id"] == task_id
    assert record["escalation_type"] == etype
    assert record["classification"] == "critical"

    # per-task 파일
    per_task_path = ws / "memory" / "events" / f"{task_id}.escalation.json"
    assert per_task_path.exists(), "Per-task escalation JSON file should exist"
    per_task_data = json.loads(per_task_path.read_text(encoding="utf-8"))
    assert isinstance(per_task_data, dict), "Per-task file should contain a JSON object"


# ---------------------------------------------------------------------------
# test_08: 회장 §9 — critical fixture 6개 replay
# ---------------------------------------------------------------------------

def test_08_replay_critical_fixtures(tmp_path: Path) -> None:
    """회장 §9 6개 critical fixture 모두 process_event → classification == 'critical'."""
    fixtures = [
        # (설명, event_type, expected_escalation_type, expected_severity)
        (
            "forbidden path",
            "FORBIDDEN_PATH_INTRUSION",
            "FORBIDDEN_PATH_INTRUSION",
            "HIGH_CORE",
        ),
        (
            "replacement failure",
            "REPLACEMENT_PR_AUTO_CREATION_FAILED_FOR_CONTAMINATED_DIFF",
            "REPLACEMENT_PR_AUTO_CREATION_FAILED_FOR_CONTAMINATED_DIFF",
            "HIGH_CORE",
        ),
        (
            "scope expansion",
            "GEMINI_REAL_BUG_REQUIRES_SCOPE_EXPANSION",
            "GEMINI_REAL_BUG_REQUIRES_SCOPE_EXPANSION",
            "HIGH",
        ),
        (
            "dependency cycle",
            "DEPENDENCY_CYCLE_OR_SERIAL_ONLY_COLLISION",
            "DEPENDENCY_CYCLE_OR_SERIAL_ONLY_COLLISION",
            "HIGH_CORE",
        ),
        (
            "smoke failure",
            "POST_MERGE_SMOKE_FAILED",
            "POST_MERGE_SMOKE_FAILED",
            "HIGH",
        ),
        (
            "BLOCK override insufficient",
            "BLOCK_OVERRIDE_REQUIRED_OR_REASON_INSUFFICIENT",
            "BLOCK_OVERRIDE_REQUIRED_OR_REASON_INSUFFICIENT",
            "HIGH_CORE",
        ),
    ]

    for desc, etype, expected_etype, expected_severity in fixtures:
        ws = tmp_path / f"fixture-{etype}"
        result = process_event(
            _make_event(etype, task_id=f"task-08-{etype[:10]}"),
            workspace_root=ws,
            dry_run=True,
            now=_NOW,
        )
        assert result["classification"] == "critical", (
            f"[§9 {desc}] classification should be 'critical', got {result['classification']!r}"
        )
        assert result["escalation_type"] == expected_etype, (
            f"[§9 {desc}] escalation_type mismatch: expected={expected_etype!r}, got={result['escalation_type']!r}"
        )
        assert result["severity"] == expected_severity, (
            f"[§9 {desc}] severity mismatch: expected={expected_severity!r}, got={result['severity']!r}"
        )
        assert result["packet"] is not None, f"[§9 {desc}] packet should not be None"


# ---------------------------------------------------------------------------
# test_09: 회장 §10 — auto-handled fixture 5개 replay
# ---------------------------------------------------------------------------

def test_09_replay_auto_handled_fixtures(tmp_path: Path) -> None:
    """회장 §10 5개 auto-handled fixture → classification == 'auto-handled', packet is None."""
    fixtures = [
        ("style-only Gemini", "STYLE_ONLY_GEMINI"),
        ("false-positive Gemini", "FALSE_POSITIVE_GEMINI"),
        ("outdated thread", "OUTDATED_THREAD"),
        ("clean replacement PR", "CLEAN_REPLACEMENT_PR_CREATED"),
        ("dependency satisfied", "DEPENDENCY_SATISFIED"),
    ]

    for desc, etype in fixtures:
        ws = tmp_path / f"auto-{etype}"
        result = process_event(
            _make_event(etype, task_id=f"task-09-{etype[:12]}"),
            workspace_root=ws,
            dry_run=True,
            now=_NOW,
        )
        assert result["classification"] == "auto-handled", (
            f"[§10 {desc}] classification should be 'auto-handled', got {result['classification']!r}"
        )
        assert result["packet"] is None, f"[§10 {desc}] packet should be None"
        assert result["formatted_text"] is None, f"[§10 {desc}] formatted_text should be None"
        assert result["suppression_reason"], f"[§10 {desc}] suppression_reason should not be empty"


# ---------------------------------------------------------------------------
# test_10: FORBIDDEN_PATH_INTRUSION HIGH_CORE audit 검증
# ---------------------------------------------------------------------------

def test_10_forbidden_path_high_core_audit(tmp_path: Path) -> None:
    """FORBIDDEN_PATH_INTRUSION 이벤트 처리 후:
    - severity == 'HIGH_CORE'
    - audit 라인의 escalation_type == 'FORBIDDEN_PATH_INTRUSION'
    """
    ws = tmp_path / "fpi-ws"
    result = process_event(
        _make_event("FORBIDDEN_PATH_INTRUSION", task_id="task-10"),
        workspace_root=ws,
        dry_run=True,
        now=_NOW,
    )

    assert result["severity"] == "HIGH_CORE", (
        f"severity should be 'HIGH_CORE', got {result['severity']!r}"
    )

    lines = _read_audit_lines(ws)
    assert len(lines) >= 1, "Expected at least 1 audit line"
    record = lines[-1]
    assert record["escalation_type"] == "FORBIDDEN_PATH_INTRUSION", (
        f"audit escalation_type should be 'FORBIDDEN_PATH_INTRUSION', got {record['escalation_type']!r}"
    )
    assert record["severity"] == "HIGH_CORE", (
        f"audit severity should be 'HIGH_CORE', got {record['severity']!r}"
    )


# ---------------------------------------------------------------------------
# test_11: POST_MERGE_SMOKE_FAILED → audit JSONL append (+1)
# ---------------------------------------------------------------------------

def test_11_smoke_failure_critical_seven(tmp_path: Path) -> None:
    """POST_MERGE_SMOKE_FAILED 처리 → classification == 'critical', audit JSONL에 라인 추가."""
    ws = tmp_path / "smoke-ws"

    # 사전 다른 이벤트 1건 처리 (baseline)
    process_event(
        _make_event("REPLACEMENT_PR_FAILED", task_id="task-11-pre"),
        workspace_root=ws,
        dry_run=True,
        now=_NOW,
    )
    lines_before = _read_audit_lines(ws)

    # smoke 이벤트 처리
    result = process_event(
        _make_event("POST_MERGE_SMOKE_FAILED", task_id="task-11-smoke"),
        workspace_root=ws,
        dry_run=True,
        now=_NOW,
    )

    assert result["classification"] == "critical", (
        f"classification should be 'critical', got {result['classification']!r}"
    )

    lines_after = _read_audit_lines(ws)
    assert len(lines_after) == len(lines_before) + 1, (
        f"Expected {len(lines_before) + 1} audit lines after smoke event, got {len(lines_after)}"
    )
    last_record = lines_after[-1]
    assert last_record["escalation_type"] == "POST_MERGE_SMOKE_FAILED", (
        f"Last audit line escalation_type should be 'POST_MERGE_SMOKE_FAILED'"
    )


# ---------------------------------------------------------------------------
# test_12: STYLE_ONLY_GEMINI → auto-handled, audit 태깅 검증
# ---------------------------------------------------------------------------

def test_12_style_only_audit_tagged(tmp_path: Path) -> None:
    """STYLE_ONLY_GEMINI 처리 후:
    - classification == 'auto-handled'
    - audit 라인의 classification == 'auto-handled'
    - suppressed_reason 필드 비어있지 않음
    """
    ws = tmp_path / "style-only-ws"
    result = process_event(
        _make_event("STYLE_ONLY_GEMINI", task_id="task-12"),
        workspace_root=ws,
        dry_run=True,
        now=_NOW,
    )

    assert result["classification"] == "auto-handled", (
        f"classification should be 'auto-handled', got {result['classification']!r}"
    )

    lines = _read_audit_lines(ws)
    assert len(lines) >= 1, "Expected at least 1 audit line"
    record = lines[-1]
    assert record["classification"] == "auto-handled", (
        f"audit classification should be 'auto-handled', got {record['classification']!r}"
    )
    assert record.get("suppressed_reason"), (
        "audit suppressed_reason should not be empty for auto-handled"
    )


# ---------------------------------------------------------------------------
# test_13 (보너스 G1): Legacy critical map — 7개 모두 canonical 매핑
# ---------------------------------------------------------------------------

def test_13_legacy_critical_compat(tmp_path: Path) -> None:
    """LEGACY_CRITICAL_MAP의 7개 legacy 이름 입력 시 모두 classification == 'critical'.
    escalation_type은 canonical enum value (매핑 후 이름) 와 일치해야 한다."""
    assert len(LEGACY_CRITICAL_MAP) == 7, (
        f"LEGACY_CRITICAL_MAP should have 7 entries, got {len(LEGACY_CRITICAL_MAP)}"
    )

    for legacy_key, canonical_enum in LEGACY_CRITICAL_MAP.items():
        ws = tmp_path / f"legacy-{legacy_key}"
        result = process_event(
            _make_event(legacy_key, task_id=f"task-legacy-{legacy_key[:8]}"),
            workspace_root=ws,
            dry_run=True,
            now=_NOW,
        )
        assert result["classification"] == "critical", (
            f"[legacy={legacy_key}] classification should be 'critical', got {result['classification']!r}"
        )
        assert result["escalation_type"] == canonical_enum.value, (
            f"[legacy={legacy_key}] escalation_type should be canonical {canonical_enum.value!r}, "
            f"got {result['escalation_type']!r}"
        )
        assert result["packet"] is not None, f"[legacy={legacy_key}] packet should not be None"
        assert result["severity"] is not None, f"[legacy={legacy_key}] severity should not be None"


# ---------------------------------------------------------------------------
# test_14 (보너스): Audit JSONL 다건 파싱 검증
# ---------------------------------------------------------------------------

def test_14_audit_jsonl_parseable(tmp_path: Path) -> None:
    """3건의 다른 이벤트 처리 후 JSONL 파일 line-by-line json.loads → 3개 모두 파싱 성공."""
    ws = tmp_path / "multi-ws"
    events = [
        _make_event("FORBIDDEN_PATH_INTRUSION", task_id="task-14-a"),
        _make_event("REPLACEMENT_PR_FAILED", task_id="task-14-b"),
        _make_event("POST_MERGE_SMOKE_FAILED", task_id="task-14-c"),
    ]

    for event in events:
        process_event(event, workspace_root=ws, dry_run=True, now=_NOW)

    global_path = _global_audit_path(ws)
    assert global_path.exists(), "Global audit JSONL should exist"

    raw_lines = [
        ln.strip()
        for ln in global_path.read_text(encoding="utf-8").splitlines()
        if ln.strip()
    ]
    assert len(raw_lines) == 3, f"Expected 3 audit lines, got {len(raw_lines)}"

    for i, line in enumerate(raw_lines):
        try:
            record = json.loads(line)
        except json.JSONDecodeError as exc:
            pytest.fail(f"Line {i + 1} failed json.loads: {exc}\nLine content: {line!r}")
        assert "ts" in record, f"Line {i + 1} missing 'ts' key"
        assert "task_id" in record, f"Line {i + 1} missing 'task_id' key"
        assert "classification" in record, f"Line {i + 1} missing 'classification' key"
