"""Unit tests for src/review_parse (ORCH-046).

Covers the defensive extractors that pull verbatim must-fix text out of the
reviewer / tester artifacts for embedding into the rollback ``task_desc``:

  - extract_review_findings  (12-review.md, ## Findings -> P0/P1)
  - extract_test_failures    (13-test-report.md, pytest/FAIL/Итог excerpt)

Both must NEVER raise (return "" on missing/broken/empty input) and must ignore
template placeholders / non-must-fix severities. See 04-test-plan.yaml (TC-01..08).
"""

import os
import tempfile

import pytest

from src.review_parse import (
    extract_review_findings,
    extract_test_failures,
    MAX_FINDINGS_CHARS,
    MAX_FAILURES_CHARS,
)


@pytest.fixture
def write_file(tmp_path):
    def _w(name: str, content: str) -> str:
        p = tmp_path / name
        p.write_text(content, encoding="utf-8")
        return str(p)
    return _w


# ---------------------------------------------------------------------------
# extract_review_findings
# ---------------------------------------------------------------------------
_REVIEW_WITH_FINDINGS = """---
type: review
work_item_id: ORCH-046
verdict: REQUEST_CHANGES
version: 1
---

# Review ORCH-046

## Summary
Несколько проблем.

## Findings

### P0 — Blocker
- [ ] Документация не обновлена при изменении src/review_parse.py

### P1 — Must fix
- [ ] extract_test_failures не обрабатывает пустой отчёт
- [ ] Отсутствует docstring у _section_body

### P2 — Should fix
- [ ] Переименовать переменную blocks в more descriptive

## Документация
Требует обновления README.
"""


class TestExtractReviewFindings:
    def test_tc01_returns_verbatim_p0_p1(self, write_file):
        """TC-01: P0/P1 findings present -> verbatim text returned (AC-1, AC-5)."""
        path = write_file("12-review.md", _REVIEW_WITH_FINDINGS)
        out = extract_review_findings(path)
        # P0 + P1 verbatim items present.
        assert "Документация не обновлена при изменении src/review_parse.py" in out
        assert "extract_test_failures не обрабатывает пустой отчёт" in out
        assert "Отсутствует docstring у _section_body" in out
        # Subsection headers preserved.
        assert "P0" in out and "P1" in out
        # P2 must NOT leak in.
        assert "Переименовать переменную" not in out

    def test_tc02_only_p2_p3_returns_empty(self, write_file):
        """TC-02: only P2/P3 (no must-fix P0/P1) -> '' (AC-5)."""
        content = """---
verdict: REQUEST_CHANGES
---

## Findings

### P0 — Blocker
- [ ] <описание> (если есть)

### P1 — Must fix
- [ ] <описание> (если есть)

### P2 — Should fix
- [ ] Косметика в naming
"""
        path = write_file("12-review.md", content)
        assert extract_review_findings(path) == ""

    def test_tc03_missing_file_returns_empty(self):
        """TC-03: non-existent path -> '' without raising (AC-4)."""
        missing = os.path.join(tempfile.gettempdir(), "no-such-review-orch046.md")
        assert extract_review_findings(missing) == ""

    def test_tc04_broken_or_no_findings_section_returns_empty(self, write_file):
        """TC-04: empty file / markdown without ## Findings -> '' (AC-4, AC-5)."""
        # Empty file.
        assert extract_review_findings(write_file("empty.md", "")) == ""
        # No Findings section.
        no_section = "# Review\n\n## Summary\nвсё хорошо\n"
        assert extract_review_findings(write_file("nofind.md", no_section)) == ""
        # Broken YAML frontmatter (unterminated) — body parsing still graceful.
        broken = "---\nverdict: [unclosed\n# Review\nno findings here\n"
        assert extract_review_findings(write_file("broken.md", broken)) == ""

    def test_tc05_long_findings_truncated(self, write_file):
        """TC-05: very long findings truncated to limit with marker (AC-1)."""
        big_item = "- [ ] " + ("x" * 5000)
        content = f"## Findings\n\n### P0 — Blocker\n{big_item}\n"
        path = write_file("12-review.md", content)
        out = extract_review_findings(path)
        assert len(out) <= MAX_FINDINGS_CHARS + len("\n…(truncated)")
        assert "…(truncated)" in out

    def test_case_insensitive_and_dash_tolerant_header(self, write_file):
        """P0/P1 recognized regardless of case / dash style."""
        content = """## Findings

### p0 - blocker
- [ ] Нижний регистр заголовка

### P1 — Must fix
- [ ] Em-dash заголовок
"""
        out = extract_review_findings(write_file("12-review.md", content))
        assert "Нижний регистр заголовка" in out
        assert "Em-dash заголовок" in out

    def test_never_raises_on_directory_path(self, tmp_path):
        """Passing a directory path must not raise -> ''."""
        assert extract_review_findings(str(tmp_path)) == ""


# ---------------------------------------------------------------------------
# extract_test_failures
# ---------------------------------------------------------------------------
_REPORT_FAIL = """---
type: test-report
work_item_id: ORCH-046
result: FAIL
---

# Test Report — ORCH-046

## Окружение
- Python: 3.12

## Результаты

| TC ID | Описание | Результат |
|-------|----------|-----------|
| TC-01 | парсер findings | PASS |
| TC-09 | rollback task_desc | FAIL |

## Вывод pytest
FAILED tests/test_stage_engine.py::TestReviewerRequestChanges::test_embed - AssertionError
1 failed, 40 passed in 2.13s

## Итог
FAIL
"""


class TestExtractTestFailures:
    def test_tc06_extracts_pytest_output(self, write_file):
        """TC-06: relevant body excerpt (pytest output) from FAIL report (AC-2, AC-5)."""
        path = write_file("13-test-report.md", _REPORT_FAIL)
        out = extract_test_failures(path)
        assert "FAILED tests/test_stage_engine.py" in out
        assert "1 failed, 40 passed" in out

    def test_priority_falls_back_to_fail_rows(self, write_file):
        """No pytest section -> FAIL rows of the results table are used."""
        content = """---
result: FAIL
---

## Результаты

| TC ID | Описание | Результат |
|-------|----------|-----------|
| TC-01 | ok | PASS |
| TC-09 | broken | FAIL |

## Итог
FAIL
"""
        out = extract_test_failures(write_file("13-test-report.md", content))
        assert "TC-09" in out
        assert "broken" in out
        # PASS rows are not failure-relevant.
        assert "TC-01" not in out

    def test_priority_falls_back_to_itog(self, write_file):
        """No pytest section and no FAIL rows -> Итог summary is used."""
        content = """---
result: FAIL
---

## Итог
Регресс упал: смотрите CI лог.
"""
        out = extract_test_failures(write_file("13-test-report.md", content))
        assert "Регресс упал" in out

    def test_tc07_missing_file_returns_empty(self):
        """TC-07: non-existent path -> '' without raising (AC-4)."""
        missing = os.path.join(tempfile.gettempdir(), "no-such-report-orch046.md")
        assert extract_test_failures(missing) == ""

    def test_tc08_broken_or_empty_report_returns_empty(self, write_file):
        """TC-08: empty / section-less report -> '' without raising (AC-4, AC-5)."""
        assert extract_test_failures(write_file("empty.md", "")) == ""
        no_sections = "---\nresult: FAIL\n---\n\n# Test Report\nничего полезного\n"
        assert extract_test_failures(write_file("nosec.md", no_sections)) == ""

    def test_long_failures_truncated(self, write_file):
        """Long pytest output is truncated to the limit with a marker."""
        big = "x" * 5000
        content = f"## Вывод pytest\n{big}\n"
        out = extract_test_failures(write_file("13-test-report.md", content))
        assert len(out) <= MAX_FAILURES_CHARS + len("\n…(truncated)")
        assert "…(truncated)" in out

    def test_never_raises_on_directory_path(self, tmp_path):
        assert extract_test_failures(str(tmp_path)) == ""
