"""JUnit XML test report parser.

Parses JUnit XML test reports using Python's standard library xml.etree.ElementTree.
Handles both <testsuites> and bare <testsuite> root elements.
"""

import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Any, Dict, List, Optional, Union


class ParseResult:
    """Result of parsing a JUnit XML report."""

    def __init__(
        self,
        success: bool,
        total: int = 0,
        passed: int = 0,
        failed: int = 0,
        errored: int = 0,
        skipped: int = 0,
        test_cases: Optional[List[Dict[str, Any]]] = None,
        error_message: Optional[str] = None,
    ):
        self.success = success
        self.total = total
        self.passed = passed
        self.failed = failed
        self.errored = errored
        self.skipped = skipped
        self.test_cases = test_cases or []
        self.error_message = error_message

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for JSON serialization."""
        result = {
            "success": self.success,
            "total": self.total,
            "passed": self.passed,
            "failed": self.failed,
            "errored": self.errored,
            "skipped": self.skipped,
            "test_cases": self.test_cases,
        }
        if self.error_message:
            result["error_message"] = self.error_message
        return result


def _truncate_message(text: Optional[str], max_length: int = 2000) -> str:
    """Truncate message text to max_length characters."""
    if not text:
        return ""
    if len(text) <= max_length:
        return text
    return text[:max_length] + "... (truncated)"


def _parse_test_case(testcase_elem: ET.Element) -> Dict[str, Any]:
    """Parse a single <testcase> element.

    Args:
        testcase_elem: ElementTree Element representing a test case

    Returns:
        Dictionary with test case details: name, classname, status, duration_seconds, message
    """
    name = testcase_elem.get("name", "")
    classname = testcase_elem.get("classname", "")
    time_str = testcase_elem.get("time", "0")

    # Parse duration as float, default to 0 on error
    try:
        duration = float(time_str)
    except (ValueError, TypeError):
        duration = 0.0

    # Determine status from child elements
    failure = testcase_elem.find("failure")
    error = testcase_elem.find("error")
    skipped = testcase_elem.find("skipped")

    if failure is not None:
        status = "fail"
        # Try message attribute first, then text content
        message = failure.get("message") or failure.text or ""
        message = _truncate_message(message)
    elif error is not None:
        status = "error"
        message = error.get("message") or error.text or ""
        message = _truncate_message(message)
    elif skipped is not None:
        status = "skip"
        message = skipped.get("message") or skipped.text or ""
        message = _truncate_message(message)
    else:
        status = "pass"
        message = ""

    return {
        "name": name,
        "classname": classname,
        "status": status,
        "duration_seconds": duration,
        "message": message,
    }


def _parse_testsuite(testsuite_elem: ET.Element) -> List[Dict[str, Any]]:
    """Parse a single <testsuite> element and extract all test cases.

    Args:
        testsuite_elem: ElementTree Element representing a test suite

    Returns:
        List of test case dictionaries
    """
    test_cases = []
    for testcase in testsuite_elem.findall("testcase"):
        test_cases.append(_parse_test_case(testcase))
    return test_cases


def parse_junit_xml(source: Union[str, Path]) -> ParseResult:
    """Parse JUnit XML test report from file path or XML string.

    Args:
        source: File path (str or Path) or XML string

    Returns:
        ParseResult with aggregate counts and list of test cases
    """
    try:
        # Determine if source is a file path or XML string
        if isinstance(source, Path):
            source_str = str(source)
        else:
            source_str = source

        # Try to parse as file first if it looks like a path
        if isinstance(source, Path) or (
            isinstance(source, str) and not source.strip().startswith("<")
        ):
            # Treat as file path
            path = Path(source_str)
            if not path.exists():
                return ParseResult(
                    success=False,
                    error_message=f"File not found: {source_str}",
                )
            tree = ET.parse(path)
            root = tree.getroot()
        else:
            # Treat as XML string
            root = ET.fromstring(source_str)

        # Handle both <testsuites> and bare <testsuite> root
        all_test_cases: List[Dict[str, Any]] = []

        if root.tag == "testsuites":
            # Multiple test suites
            for testsuite in root.findall("testsuite"):
                all_test_cases.extend(_parse_testsuite(testsuite))
        elif root.tag == "testsuite":
            # Single test suite
            all_test_cases.extend(_parse_testsuite(root))
        else:
            return ParseResult(
                success=False,
                error_message=f"Unexpected root element: {root.tag}. Expected 'testsuites' or 'testsuite'.",
            )

        # Calculate aggregate counts
        total = len(all_test_cases)
        passed = sum(1 for tc in all_test_cases if tc["status"] == "pass")
        failed = sum(1 for tc in all_test_cases if tc["status"] == "fail")
        errored = sum(1 for tc in all_test_cases if tc["status"] == "error")
        skipped = sum(1 for tc in all_test_cases if tc["status"] == "skip")

        return ParseResult(
            success=True,
            total=total,
            passed=passed,
            failed=failed,
            errored=errored,
            skipped=skipped,
            test_cases=all_test_cases,
        )

    except ET.ParseError as e:
        return ParseResult(
            success=False,
            error_message=f"XML parse error: {str(e)}",
        )
    except Exception as e:
        return ParseResult(
            success=False,
            error_message=f"Unexpected error: {str(e)}",
        )
