"""
koci Test Report Parsers

Parses JUnit XML and coverage reports (Cobertura, lcov) to extract
test results and coverage metrics.
"""

import glob
import os
import re
import xml.etree.ElementTree as ET
from dataclasses import dataclass, field
from typing import Optional


@dataclass
class TestCase:
    """A single test case result."""
    name: str
    classname: str
    time: float = 0.0
    status: str = "passed"  # passed, failed, skipped, error
    message: Optional[str] = None
    output: Optional[str] = None


@dataclass
class JUnitResult:
    """Results from parsing JUnit XML reports."""
    tests: int = 0
    passed: int = 0
    failures: int = 0
    errors: int = 0
    skipped: int = 0
    time: float = 0.0
    test_cases: list[TestCase] = field(default_factory=list)

    @property
    def total_failed(self) -> int:
        return self.failures + self.errors

    @property
    def success(self) -> bool:
        return self.total_failed == 0

    def summary(self) -> str:
        """Generate a summary string."""
        parts = []
        if self.passed > 0:
            parts.append(f"{self.passed} passed")
        if self.failures > 0:
            parts.append(f"{self.failures} failed")
        if self.errors > 0:
            parts.append(f"{self.errors} errors")
        if self.skipped > 0:
            parts.append(f"{self.skipped} skipped")
        return f"{', '.join(parts)} ({self.time:.1f}s)"

    def get_failed_tests(self) -> list[TestCase]:
        """Get list of failed or errored test cases."""
        return [tc for tc in self.test_cases if tc.status in ('failed', 'error')]


@dataclass
class CoverageResult:
    """Results from parsing coverage reports."""
    line_rate: float = 0.0  # 0.0 to 1.0
    branch_rate: float = 0.0  # 0.0 to 1.0
    lines_covered: int = 0
    lines_total: int = 0
    branches_covered: int = 0
    branches_total: int = 0
    format: str = "unknown"  # cobertura, lcov

    @property
    def line_percent(self) -> float:
        return self.line_rate * 100

    @property
    def branch_percent(self) -> float:
        return self.branch_rate * 100

    def meets_threshold(self, threshold: float) -> bool:
        """Check if coverage meets the threshold (as percentage)."""
        return self.line_percent >= threshold

    def summary(self) -> str:
        """Generate a summary string."""
        parts = [f"{self.line_percent:.1f}% lines"]
        if self.branches_total > 0:
            parts.append(f"{self.branch_percent:.1f}% branches")
        return ', '.join(parts)


@dataclass
class ReportResult:
    """Combined result from all report types."""
    junit: Optional[JUnitResult] = None
    coverage: Optional[CoverageResult] = None

    @property
    def has_junit(self) -> bool:
        return self.junit is not None

    @property
    def has_coverage(self) -> bool:
        return self.coverage is not None


class JUnitParser:
    """
    Parser for JUnit XML test reports.

    Supports the standard JUnit XML format used by:
    - pytest (with pytest-junit)
    - Jest (with jest-junit)
    - JUnit/Java
    - Go test (with go-junit-report)
    - Many other test frameworks
    """

    def parse(self, path: str) -> JUnitResult:
        """
        Parse JUnit XML file(s).

        Args:
            path: Path to XML file or glob pattern (e.g., "test-results/*.xml")

        Returns:
            JUnitResult with aggregated test results
        """
        result = JUnitResult()

        # Expand glob pattern
        if '*' in path:
            files = glob.glob(path)
        else:
            files = [path] if os.path.exists(path) else []

        if not files:
            return result

        for filepath in files:
            self._parse_file(filepath, result)

        return result

    def _parse_file(self, filepath: str, result: JUnitResult):
        """Parse a single JUnit XML file."""
        try:
            tree = ET.parse(filepath)
            root = tree.getroot()

            # Handle both <testsuites> and <testsuite> as root
            if root.tag == 'testsuites':
                for testsuite in root.findall('testsuite'):
                    self._parse_testsuite(testsuite, result)
            elif root.tag == 'testsuite':
                self._parse_testsuite(root, result)

        except ET.ParseError:
            pass  # Skip invalid XML files

    def _parse_testsuite(self, testsuite: ET.Element, result: JUnitResult):
        """Parse a single testsuite element."""
        # Get suite-level counts
        tests = int(testsuite.get('tests', 0))
        failures = int(testsuite.get('failures', 0))
        errors = int(testsuite.get('errors', 0))
        skipped = int(testsuite.get('skipped', 0))
        time = float(testsuite.get('time', 0))

        result.tests += tests
        result.failures += failures
        result.errors += errors
        result.skipped += skipped
        result.time += time
        result.passed += tests - failures - errors - skipped

        # Parse individual test cases for details
        for testcase in testsuite.findall('testcase'):
            tc = self._parse_testcase(testcase)
            result.test_cases.append(tc)

    def _parse_testcase(self, testcase: ET.Element) -> TestCase:
        """Parse a single testcase element."""
        tc = TestCase(
            name=testcase.get('name', ''),
            classname=testcase.get('classname', ''),
            time=float(testcase.get('time', 0)),
        )

        # Check for failure
        failure = testcase.find('failure')
        if failure is not None:
            tc.status = 'failed'
            tc.message = failure.get('message', '')
            tc.output = failure.text

        # Check for error
        error = testcase.find('error')
        if error is not None:
            tc.status = 'error'
            tc.message = error.get('message', '')
            tc.output = error.text

        # Check for skipped
        skipped = testcase.find('skipped')
        if skipped is not None:
            tc.status = 'skipped'
            tc.message = skipped.get('message', '')

        return tc


class CoverageParser:
    """
    Parser for code coverage reports.

    Supports:
    - Cobertura XML format (Python coverage, Istanbul, etc.)
    - LCOV format (gcov, llvm-cov, etc.)
    """

    def parse(self, path: str, format: str = "auto") -> CoverageResult:
        """
        Parse coverage report file.

        Args:
            path: Path to coverage file
            format: Report format ("cobertura", "lcov", or "auto" to detect)

        Returns:
            CoverageResult with coverage metrics
        """
        if not os.path.exists(path):
            return CoverageResult()

        # Auto-detect format
        if format == "auto":
            format = self._detect_format(path)

        if format == "cobertura":
            return self._parse_cobertura(path)
        elif format == "lcov":
            return self._parse_lcov(path)
        else:
            return CoverageResult()

    def _detect_format(self, path: str) -> str:
        """Detect coverage format from file content."""
        try:
            with open(path, 'r') as f:
                first_line = f.readline()

            if first_line.startswith('<?xml') or '<coverage' in first_line:
                return "cobertura"
            elif first_line.startswith('TN:') or first_line.startswith('SF:'):
                return "lcov"
            else:
                # Try to parse as XML
                try:
                    ET.parse(path)
                    return "cobertura"
                except ET.ParseError:
                    return "lcov"
        except Exception:
            return "unknown"

    def _parse_cobertura(self, path: str) -> CoverageResult:
        """Parse Cobertura XML format."""
        result = CoverageResult(format="cobertura")

        try:
            tree = ET.parse(path)
            root = tree.getroot()

            # Get coverage attributes
            result.line_rate = float(root.get('line-rate', 0))
            result.branch_rate = float(root.get('branch-rate', 0))

            # Count lines
            lines_covered = 0
            lines_total = 0
            branches_covered = 0
            branches_total = 0

            for line in root.iter('line'):
                lines_total += 1
                hits = int(line.get('hits', 0))
                if hits > 0:
                    lines_covered += 1

                # Branch coverage
                if line.get('branch') == 'true':
                    condition_coverage = line.get('condition-coverage', '')
                    match = re.search(r'\((\d+)/(\d+)\)', condition_coverage)
                    if match:
                        branches_covered += int(match.group(1))
                        branches_total += int(match.group(2))

            result.lines_covered = lines_covered
            result.lines_total = lines_total
            result.branches_covered = branches_covered
            result.branches_total = branches_total

        except ET.ParseError:
            pass

        return result

    def _parse_lcov(self, path: str) -> CoverageResult:
        """Parse LCOV format."""
        result = CoverageResult(format="lcov")

        try:
            with open(path, 'r') as f:
                content = f.read()

            lines_found = 0
            lines_hit = 0
            branches_found = 0
            branches_hit = 0

            for line in content.split('\n'):
                line = line.strip()

                # Lines found/hit
                if line.startswith('LF:'):
                    lines_found += int(line[3:])
                elif line.startswith('LH:'):
                    lines_hit += int(line[3:])

                # Branches found/hit
                elif line.startswith('BRF:'):
                    branches_found += int(line[4:])
                elif line.startswith('BRH:'):
                    branches_hit += int(line[4:])

            result.lines_total = lines_found
            result.lines_covered = lines_hit
            result.branches_total = branches_found
            result.branches_covered = branches_hit

            if lines_found > 0:
                result.line_rate = lines_hit / lines_found
            if branches_found > 0:
                result.branch_rate = branches_hit / branches_found

        except Exception:
            pass

        return result


def parse_reports(
    junit_path: Optional[str] = None,
    coverage_path: Optional[str] = None,
    coverage_format: str = "auto",
) -> ReportResult:
    """
    Parse test reports and return combined results.

    Args:
        junit_path: Path to JUnit XML file(s), supports glob patterns
        coverage_path: Path to coverage report file
        coverage_format: Coverage format ("cobertura", "lcov", or "auto")

    Returns:
        ReportResult with parsed test and coverage data
    """
    result = ReportResult()

    if junit_path:
        parser = JUnitParser()
        result.junit = parser.parse(junit_path)

    if coverage_path:
        parser = CoverageParser()
        result.coverage = parser.parse(coverage_path, coverage_format)

    return result
