"""Разбор отчётов JUnit XML.

Особенности:
1. Дедупликация тестов по уникальным именам между контекстами (например, YAxUnit
   гоняет один и тот же тест в [Сервер] и [КлиентУправляемоеПриложение]).
2. Подробный вывод упавших тестов: имя теста, класс/модуль, сообщение об ошибке, контексты.
3. Проверка на 0 тестов: отчёт без тестов не считается успешным.
"""

from __future__ import annotations

import os
import xml.etree.ElementTree as ET
from dataclasses import dataclass, field
from pathlib import Path


@dataclass
class TestFailure:
    name: str
    classname: str
    message: str
    failure_type: str = ""
    details: str = ""
    contexts: list[str] = field(default_factory=list)

    @property
    def full_name(self) -> str:
        if self.classname and self.name:
            if self.classname == self.name or self.classname.endswith("." + self.name):
                return self.classname
            if self.name.startswith(self.classname + "."):
                return self.name
            return f"{self.classname}.{self.name}"
        return self.classname or self.name


@dataclass
class JUnitReport:
    total_runs: int = 0
    unique_tests_count: int = 0
    unique_failures_count: int = 0
    unique_errors_count: int = 0
    unique_passed_count: int = 0
    unique_skipped_count: int = 0
    failures: dict[str, TestFailure] = field(default_factory=dict)
    errors: dict[str, TestFailure] = field(default_factory=dict)
    passed_names: set[str] = field(default_factory=set)
    skipped_names: set[str] = field(default_factory=set)
    all_names: set[str] = field(default_factory=set)

    @property
    def is_empty(self) -> bool:
        return self.unique_tests_count == 0


def _make_unique_key(tc_class: str, tc_name: str, total_runs: int) -> str:
    if tc_class and tc_name:
        if tc_class == tc_name or tc_class.endswith("." + tc_name):
            return tc_class
        if tc_name.startswith(tc_class + "."):
            return tc_name
        return f"{tc_class}.{tc_name}"
    if tc_class:
        return tc_class
    if tc_name:
        return tc_name
    return f"unnamed_test_{total_runs}"


def parse_junit_xml(xml_content: str | bytes | Path) -> JUnitReport:
    """Разбирает JUnit XML (файл, строка или байты) с дедупликацией по именам."""
    if isinstance(xml_content, Path):
        xml_text = xml_content.read_text(encoding="utf-8", errors="replace")
    elif isinstance(xml_content, bytes):
        xml_text = xml_content.decode("utf-8", errors="replace")
    else:
        xml_text = xml_content

    xml_text = xml_text.strip()
    if not xml_text:
        return JUnitReport()

    try:
        root = ET.fromstring(xml_text)
    except ET.ParseError:
        return JUnitReport()

    report = JUnitReport()

    # Корневой элемент может быть <testsuites> или <testsuite>
    if root.tag == "testsuite":
        suites = [root]
    elif root.tag == "testsuites":
        suites = root.findall("testsuite")
    else:
        suites = root.findall(".//testsuite")
        if not suites and root.findall(".//testcase"):
            suites = [root]

    for suite in suites:
        suite_context = suite.attrib.get("context", "")

        for tc in suite.findall("testcase"):
            report.total_runs += 1
            tc_name = tc.attrib.get("name", "").strip()
            tc_class = tc.attrib.get("classname", "").strip()
            tc_context = tc.attrib.get("context", suite_context)

            unique_key = _make_unique_key(tc_class, tc_name, report.total_runs)
            report.all_names.add(unique_key)

            failure_el = tc.find("failure")
            error_el = tc.find("error")
            skipped_el = tc.find("skipped")

            if failure_el is not None:
                msg = failure_el.attrib.get("message", "").strip()
                ftype = failure_el.attrib.get("type", "").strip()
                text = (failure_el.text or "").strip()
                if not msg and text:
                    msg = text.splitlines()[0]

                if unique_key in report.failures:
                    existing = report.failures[unique_key]
                    if tc_context and tc_context not in existing.contexts:
                        existing.contexts.append(tc_context)
                    if not existing.message and msg:
                        existing.message = msg
                else:
                    report.failures[unique_key] = TestFailure(
                        name=tc_name,
                        classname=tc_class,
                        message=msg,
                        failure_type=ftype,
                        details=text,
                        contexts=[tc_context] if tc_context else [],
                    )
            elif error_el is not None:
                msg = error_el.attrib.get("message", "").strip()
                text = (error_el.text or "").strip()
                if not msg and text:
                    msg = text.splitlines()[0]
                if unique_key not in report.errors:
                    report.errors[unique_key] = TestFailure(
                        name=tc_name,
                        classname=tc_class,
                        message=msg,
                        failure_type="Error",
                        details=text,
                        contexts=[tc_context] if tc_context else [],
                    )
            elif skipped_el is not None:
                report.skipped_names.add(unique_key)
            else:
                report.passed_names.add(unique_key)

    failed_or_errored = set(report.failures.keys()) | set(report.errors.keys())
    report.passed_names -= failed_or_errored
    report.unique_tests_count = len(report.all_names)
    report.unique_failures_count = len(report.failures)
    report.unique_errors_count = len(report.errors)
    report.unique_passed_count = len(report.passed_names)
    report.unique_skipped_count = len(report.skipped_names)

    return report


def find_junit_reports(search_dir: Path) -> list[Path]:
    """Ищет файлы отчетов JUnit в каталоге."""
    candidates: list[Path] = []
    patterns = ["*junit*.xml", "report*.xml", "**/junit.xml", "**/report.xml"]
    for pat in patterns:
        for p in search_dir.glob(pat):
            if p.is_file() and p.suffix.lower() == ".xml" and p not in candidates:
                candidates.append(p)
    return sorted(candidates, key=lambda f: f.stat().st_mtime, reverse=True)


def format_junit_summary(report: JUnitReport) -> str:
    """Форматирует сводку результатов тестирования для Чекана."""
    if report.is_empty:
        return "Тестов не обнаружено (0 тестов в отчёте)"

    lines: list[str] = []
    total = report.unique_tests_count
    fail_cnt = report.unique_failures_count + report.unique_errors_count

    if fail_cnt == 0:
        lines.append(f"✓ Все тесты пройдены ({total} уникальных тестов, {report.total_runs} прогонов)")
        return "\n".join(lines)

    lines.append(f"✗ Упало тестов: {fail_cnt} из {total} уникальных (всего прогонов: {report.total_runs}):")
    for key, failure in report.failures.items():
        ctx_str = f" [{', '.join(failure.contexts)}]" if failure.contexts else ""
        msg_str = f": {failure.message}" if failure.message else ""
        lines.append(f"  · {failure.full_name}{ctx_str}{msg_str}")

    for key, err in report.errors.items():
        ctx_str = f" [{', '.join(err.contexts)}]" if err.contexts else ""
        msg_str = f": {err.message}" if err.message else ""
        lines.append(f"  · [ERROR] {err.full_name}{ctx_str}{msg_str}")

    return "\n".join(lines)
