"""
Test all acceptance criteria have corresponding tests.

Validates:
- Every AC has at least one test
- Tests are properly named/linked to ACs
- No orphaned tests (tests without ACs)
- Coverage percentage meets threshold

Architecture:
- Entities: Domain models (ACDefinition, TestCase, CoverageReport)
- Use Cases: Business logic (ACFinder, TestFinder, CoverageAnalyzer)
- Adapters: Infrastructure (YAMLReader, TestFileReader, ReportFormatter)
- Tests: Orchestration layer (pytest test functions)

Inspired by: .claude/utils/tester/ (multiple utilities)
But: Self-contained, no utility dependencies
"""

import pytest
import yaml
import re
from pathlib import Path
from typing import Dict, List, Set, Tuple, Optional
from dataclasses import dataclass, field
from collections import defaultdict

from atdd.coach.utils.repo import find_repo_root


# Path constants
REPO_ROOT = find_repo_root()
PLAN_DIR = REPO_ROOT / "plan"
PYTHON_DIR = REPO_ROOT / "python"
LIB_DIR = REPO_ROOT / "lib"
TEST_DIR = REPO_ROOT / "test"
SUPABASE_DIR = REPO_ROOT / "supabase"


# Coverage thresholds
MIN_COVERAGE_PERCENTAGE = 80


# ============================================================================
# LAYER 1: ENTITIES (Domain Models)
# ============================================================================


@dataclass
class ACDefinition:
    """
    Acceptance Criterion entity.

    Represents a single acceptance criterion from the plan directory.
    Immutable domain model.
    """
    urn: str
    wagon: str
    wmbt: str
    wmbt_file: str
    purpose: str
    file_path: str

    @property
    def category(self) -> str:
        """Extract test category from URN (e.g., UNIT, HTTP, GOLDEN)."""
        match = re.search(r'acc:[a-z\-]+:([A-Z0-9]+)-([A-Z]+)-\d{3}', self.urn)
        if match:
            return match.group(2)
        return "UNKNOWN"

    @property
    def suggested_test_file(self) -> str:
        """Suggest where the test file should be created."""
        return f"python/{self.wagon}/test_{self.wmbt_file}.py"


@dataclass
class TestCase:
    """
    Test case entity.

    Represents a single test function from a test file.
    """
    name: str
    file_path: str
    ac_reference: Optional[str] = None


@dataclass
class CoverageReport:
    """
    Coverage analysis report entity.

    Aggregates all coverage data for reporting.
    """
    total_acs: int
    covered_acs: int
    missing_acs: List[ACDefinition]
    wagon_stats: Dict[str, Dict] = field(default_factory=dict)
    category_stats: Dict[str, int] = field(default_factory=dict)

    @property
    def coverage_percentage(self) -> float:
        """Calculate coverage percentage."""
        if self.total_acs == 0:
            return 0.0
        return (self.covered_acs / self.total_acs) * 100

    @property
    def missing_count(self) -> int:
        """Count of missing ACs."""
        return len(self.missing_acs)


# ============================================================================
# LAYER 2: USE CASES (Business Logic)
# ============================================================================


class ACFinder:
    """
    Use case: Find all acceptance criteria in the repository.

    Scans plan directory for WMBT files and extracts AC definitions.
    """

    def __init__(self, plan_dir: Path):
        self.plan_dir = plan_dir

    def find_all(self) -> List[ACDefinition]:
        """Find all acceptance criteria."""
        if not self.plan_dir.exists():
            return []

        acs = []

        for yaml_file in self.plan_dir.rglob("*.yaml"):
            # Skip wagon manifest files (start with underscore)
            if yaml_file.name.startswith('_'):
                continue

            try:
                with open(yaml_file, 'r', encoding='utf-8') as f:
                    data = yaml.safe_load(f)
            except Exception:
                continue

            # Check if this is a WMBT file with acceptances
            if isinstance(data, dict) and 'acceptances' in data:
                wmbt_urn = data.get('urn', 'unknown')
                wagon_name = yaml_file.parent.name

                for acceptance in data.get('acceptances', []):
                    identity = acceptance.get('identity', {})
                    urn = identity.get('urn')

                    if urn:
                        ac = ACDefinition(
                            urn=urn,
                            wagon=wagon_name,
                            wmbt=wmbt_urn,
                            wmbt_file=yaml_file.stem,
                            purpose=identity.get('purpose', ''),
                            file_path=str(yaml_file.relative_to(REPO_ROOT))
                        )
                        acs.append(ac)

        return acs


class TestFinder:
    """
    Use case: Find all test cases in the repository.

    Scans test directories for test files and extracts test functions.
    """

    def __init__(self, python_dir: Path, lib_dir: Path):
        self.python_dir = python_dir
        self.lib_dir = lib_dir

    def find_python_tests(self) -> List[TestCase]:
        """Find all Python test cases."""
        if not self.python_dir.exists():
            return []

        tests = []

        for test_file in self.python_dir.rglob("test_*.py"):
            try:
                with open(test_file, 'r', encoding='utf-8') as f:
                    content = f.read()
            except Exception:
                continue

            # Extract test function names
            test_functions = re.findall(r'def\s+(test_\w+)\s*\(', content)

            rel_path = str(test_file.relative_to(REPO_ROOT))

            for test_name in test_functions:
                # Extract AC reference from test name or docstring
                ac_ref = self._extract_ac_reference(content, test_name, rel_path)

                test = TestCase(
                    name=test_name,
                    file_path=rel_path,
                    ac_reference=ac_ref
                )
                tests.append(test)

        return tests

    def find_dart_tests(self) -> List[TestCase]:
        """Find all Dart test cases."""
        if not TEST_DIR.exists():
            return []

        tests = []

        for test_file in TEST_DIR.rglob("*_test.dart"):
            try:
                with open(test_file, 'r', encoding='utf-8') as f:
                    content = f.read()
            except Exception:
                continue

            # Extract AC reference from file comment or test name
            ac_ref = self._extract_dart_ac_reference(content, test_file.name)

            if ac_ref:
                # Dart tests are file-based, use filename as test name
                test_name = test_file.stem  # e.g., "ac_http_001_foundations_api_endpoint_accessible_test"

                rel_path = str(test_file.relative_to(REPO_ROOT))

                test = TestCase(
                    name=test_name,
                    file_path=rel_path,
                    ac_reference=ac_ref
                )
                tests.append(test)

        return tests

    def _extract_dart_ac_reference(self, content: str, filename: str) -> Optional[str]:
        """Extract AC reference from Dart test file."""
        # Try comment at top of file (// urn: acc:wagon:URN)
        comment_match = re.search(r'//\s*urn:\s*(acc:[a-z][a-z0-9\-]*:[A-Z0-9]+-[A-Z0-9]+-\d{3}(?:-[a-z0-9-]+)?)', content, re.IGNORECASE)
        if comment_match:
            return comment_match.group(1)

        # Try extracting from filename pattern (ac_http_001_...)
        filename_match = re.search(r'ac_([a-z0-9]+)_(\d{3})', filename.lower())
        if filename_match:
            # This won't give us the full URN, but we can try to find it in the content
            pass

        return None

    def find_typescript_tests(self) -> List[TestCase]:
        """Find all TypeScript test cases per conventions.

        Scans (aligns with Python structure):
        1. supabase/functions/{wagon}/{feature}/test/ (preferred, mirrors Python)
        2. e2e/{train}/ (E2E tests organized by user journey, spans multiple wagons)
        3. supabase/functions/{feature}/test/ (deprecated legacy structure)
        """
        tests = []

        supabase_functions = REPO_ROOT / "supabase" / "functions"

        # Scan preferred structure: supabase/functions/{wagon}/{feature}/test/
        if supabase_functions.exists():
            for wagon_dir in supabase_functions.iterdir():
                if wagon_dir.is_dir():
                    # Check for {wagon}/{feature}/test/ pattern (preferred)
                    for feature_dir in wagon_dir.iterdir():
                        if feature_dir.is_dir():
                            test_dir = feature_dir / "test"
                            if test_dir.exists():
                                tests.extend(self._scan_ts_directory(test_dir))

                    # Also check deprecated flat {wagon}/test/ pattern
                    wagon_test_dir = wagon_dir / "test"
                    if wagon_test_dir.exists():
                        tests.extend(self._scan_ts_directory(wagon_test_dir))

        # Note: Legacy flat structure (preload-cards, validate-card, etc.) would be
        # caught by the wagon-level scan above if they had test/ directories.
        # No additional scanning needed since those functions are deprecated.

        # Scan e2e/{train}/ directories (E2E tests by user journey)
        e2e_dir = REPO_ROOT / "e2e"
        if e2e_dir.exists():
            for train_dir in e2e_dir.iterdir():
                if train_dir.is_dir():
                    tests.extend(self._scan_ts_directory(train_dir))

        return tests

    def _scan_ts_directory(self, directory: Path) -> List[TestCase]:
        """Scan a directory for TypeScript test files."""
        tests = []

        # Look for .test.ts or .test.tsx files
        for pattern in ["*.test.ts", "*.test.tsx"]:
            for test_file in directory.rglob(pattern):
                try:
                    with open(test_file, 'r', encoding='utf-8') as f:
                        content = f.read()
                except Exception:
                    continue

                # Extract AC reference from file comment
                ac_ref = self._extract_typescript_ac_reference(content, test_file.name)

                if ac_ref:
                    # TypeScript tests are file-based, use filename as test name
                    test_name = test_file.stem  # e.g., "c004-e2e-019-user-connection.spec"

                    rel_path = str(test_file.relative_to(REPO_ROOT))

                    test = TestCase(
                        name=test_name,
                        file_path=rel_path,
                        ac_reference=ac_ref
                    )
                    tests.append(test)

        return tests

    def _extract_typescript_ac_reference(self, content: str, filename: str) -> Optional[str]:
        """Extract AC reference from TypeScript test file."""
        # Try comment at top of file (// urn: acc:wagon:URN or /* urn: acc:wagon:URN */)
        comment_match = re.search(r'(?://|/\*)\s*urn:\s*(acc:[a-z][a-z0-9\-]*:[A-Z0-9]+-[A-Z0-9]+-\d{3}(?:-[a-z0-9-]+)?)', content, re.IGNORECASE)
        if comment_match:
            return comment_match.group(1)

        # Try JSDoc style (@urn acc:wagon:URN)
        jsdoc_match = re.search(r'@urn\s+(acc:[a-z][a-z0-9\-]*:[A-Z0-9]+-[A-Z0-9]+-\d{3}(?:-[a-z0-9-]+)?)', content, re.IGNORECASE)
        if jsdoc_match:
            return jsdoc_match.group(1)

        return None

    def _extract_ac_reference(self, content: str, test_name: str, file_path: str) -> Optional[str]:
        """Extract AC reference from header comment, docstring, or test name."""
        # Full URN pattern with optional slug suffix
        urn_pattern = r'acc:[a-z][a-z0-9\-]*:[A-Z0-9]+-[A-Z0-9]+-\d{3}(?:-[a-z0-9-]+)?'

        # Priority 1: Try header comment (# URN: acc:...)
        header_match = re.search(r'#\s*URN:\s*(' + urn_pattern + r')', content, re.IGNORECASE)
        if header_match:
            return header_match.group(1)

        # Priority 2: Try function docstring
        pattern = f'def {test_name}.*?"""(.*?)"""'
        match = re.search(pattern, content, re.DOTALL)
        if match:
            docstring = match.group(1)
            ac_match = re.search(urn_pattern, docstring, re.IGNORECASE)
            if ac_match:
                return ac_match.group(0)

        # Priority 3: Try module docstring (at start of file)
        module_docstring_match = re.match(
            r'^\s*"""(.*?)"""',
            content,
            re.DOTALL
        )
        if module_docstring_match:
            docstring = module_docstring_match.group(1)
            ac_match = re.search(urn_pattern, docstring, re.IGNORECASE)
            if ac_match:
                return ac_match.group(0)

        # Priority 4: Fall back to test name pattern (partial ref)
        match = re.search(r'AC[-_]([A-Z0-9]+)[-_](\d{3})', test_name.upper())
        if match:
            return f"AC-{match.group(1)}-{match.group(2)}"

        match = re.search(r'(?:test_)?ac_(\d{3})', test_name.lower())
        if match:
            return f"AC-{match.group(1)}"

        return None


class CoverageAnalyzer:
    """
    Use case: Analyze test coverage of acceptance criteria.

    Maps tests to ACs and generates coverage reports.
    """

    def __init__(self, acs: List[ACDefinition], tests: List[TestCase]):
        self.acs = acs
        self.tests = tests
        self._ac_map = {ac.urn: ac for ac in acs}
        self._test_map = self._build_test_map()

    def _build_test_map(self) -> Dict[str, List[TestCase]]:
        """Build map of AC URN to test cases."""
        test_map = defaultdict(list)

        for test in self.tests:
            if test.ac_reference:
                test_map[test.ac_reference].append(test)

        return test_map

    def analyze(self) -> CoverageReport:
        """Analyze coverage and generate report."""
        missing_acs = []

        for ac in self.acs:
            if ac.urn not in self._test_map:
                missing_acs.append(ac)

        # Calculate wagon-level stats
        wagon_stats = self._calculate_wagon_stats(missing_acs)

        # Calculate category stats
        category_stats = self._calculate_category_stats(missing_acs)

        report = CoverageReport(
            total_acs=len(self.acs),
            covered_acs=len(self.acs) - len(missing_acs),
            missing_acs=missing_acs,
            wagon_stats=wagon_stats,
            category_stats=category_stats
        )

        return report

    def _calculate_wagon_stats(self, missing_acs: List[ACDefinition]) -> Dict[str, Dict]:
        """Calculate coverage statistics per wagon."""
        wagon_totals = defaultdict(int)
        wagon_missing = defaultdict(lambda: {'count': 0, 'acs': []})

        # Count total ACs per wagon
        for ac in self.acs:
            wagon_totals[ac.wagon] += 1

        # Count missing ACs per wagon
        for ac in missing_acs:
            wagon_missing[ac.wagon]['count'] += 1
            wagon_missing[ac.wagon]['acs'].append(ac)

        # Build stats
        stats = {}
        for wagon, total in wagon_totals.items():
            missing = wagon_missing[wagon]['count']
            covered = total - missing
            coverage = (covered / total * 100) if total > 0 else 0

            stats[wagon] = {
                'total': total,
                'covered': covered,
                'missing': missing,
                'coverage': coverage,
                'acs': wagon_missing[wagon]['acs']
            }

        return stats

    def _calculate_category_stats(self, missing_acs: List[ACDefinition]) -> Dict[str, int]:
        """Calculate missing tests by category."""
        category_counts = defaultdict(int)

        for ac in missing_acs:
            category_counts[ac.category] += 1

        return dict(category_counts)

    def find_orphaned_tests(self) -> List[TestCase]:
        """Find tests that reference non-existent ACs."""
        orphaned = []

        for test in self.tests:
            if test.ac_reference and test.ac_reference not in self._ac_map:
                # Skip contract compliance tests - they validate schemas, not ACs
                if 'contract_compliance' not in test.file_path:
                    orphaned.append(test)

        return orphaned


# ============================================================================
# LAYER 3: ADAPTERS (Presentation)
# ============================================================================


class ReportFormatter:
    """
    Adapter: Format coverage reports for output.

    Converts coverage report entities into human-readable text.
    """

    @staticmethod
    def format_detailed_report(report: CoverageReport) -> str:
        """Format comprehensive coverage gap report."""
        lines = []

        # Header
        lines.append("=" * 70)
        lines.append("COVERAGE GAP ANALYSIS - Detailed Report")
        lines.append("=" * 70)
        lines.append("")

        # Overall summary
        lines.append("📊 OVERALL COVERAGE")
        lines.append(f"   Total ACs: {report.total_acs}")
        lines.append(f"   Covered: {report.covered_acs}")
        lines.append(f"   Missing: {report.missing_count}")
        lines.append(f"   Coverage: {report.coverage_percentage:.1f}%")
        lines.append(f"   Threshold: {MIN_COVERAGE_PERCENTAGE}%")
        lines.append("")

        # Wagon-level coverage
        if report.wagon_stats:
            lines.append("=" * 70)
            lines.append("📦 COVERAGE BY WAGON")
            lines.append("=" * 70)
            lines.append("")

            # Sort by missing count (worst first)
            sorted_wagons = sorted(
                report.wagon_stats.items(),
                key=lambda x: x[1]['missing'],
                reverse=True
            )

            for wagon, stats in sorted_wagons:
                if stats['missing'] == 0:
                    continue

                lines.append(f"🚂 {wagon}")
                lines.append(f"   Coverage: {stats['coverage']:.1f}% ({stats['covered']}/{stats['total']})")
                lines.append(f"   Missing: {stats['missing']} ACs")
                lines.append("")

        # Category breakdown
        if report.category_stats:
            lines.append("=" * 70)
            lines.append("🏷️  MISSING TESTS BY CATEGORY")
            lines.append("=" * 70)
            lines.append("")

            for category in sorted(report.category_stats.keys()):
                count = report.category_stats[category]
                lines.append(f"   {category}: {count} missing")
            lines.append("")

        # Detailed missing ACs
        if report.missing_acs:
            lines.append("=" * 70)
            lines.append("📋 ALL MISSING ACCEPTANCE CRITERIA")
            lines.append("=" * 70)
            lines.append("")

            # Group by wagon
            wagon_groups = defaultdict(list)
            for ac in report.missing_acs:
                wagon_groups[ac.wagon].append(ac)

            # Sort wagons by missing count
            sorted_wagons = sorted(
                wagon_groups.items(),
                key=lambda x: len(x[1]),
                reverse=True
            )

            for wagon, acs in sorted_wagons:
                lines.append("")
                lines.append("=" * 70)
                lines.append(f"WAGON: {wagon} ({len(acs)} missing tests)")
                lines.append("=" * 70)
                lines.append("")

                # Sort ACs by URN
                sorted_acs = sorted(acs, key=lambda x: x.urn)

                for ac in sorted_acs:
                    lines.append(f"URN: {ac.urn}")
                    lines.append(f"  Category: {ac.category}")
                    lines.append(f"  WMBT: {ac.wmbt}")
                    lines.append(f"  Purpose: {ac.purpose}")
                    lines.append(f"  Spec File: {ac.file_path}")
                    lines.append(f"  Suggested Test: {ac.suggested_test_file}")
                    lines.append("")

        # Recommendations
        lines.append("=" * 70)
        lines.append("💡 RECOMMENDATIONS")
        lines.append("=" * 70)
        lines.append("")

        if report.wagon_stats:
            lines.append("Priority Order (by missing test count):")
            sorted_wagons = sorted(
                report.wagon_stats.items(),
                key=lambda x: x[1]['missing'],
                reverse=True
            )

            for i, (wagon, stats) in enumerate(sorted_wagons[:5], 1):
                if stats['missing'] == 0:
                    continue
                lines.append(f"  {i}. {wagon}: {stats['missing']} missing tests")

            lines.append("")

        lines.append("Next Steps:")
        lines.append("  1. Focus on high-priority wagons first")
        lines.append("  2. Group test creation by WMBT file")
        lines.append("  3. Use suggested test locations above")
        lines.append("  4. Reference spec files for AC details")
        lines.append("")

        return "\n".join(lines)

    @staticmethod
    def format_orphaned_report(orphaned: List[TestCase]) -> str:
        """Format orphaned tests report."""
        lines = []

        lines.append(f"Found {len(orphaned)} orphaned tests:")
        lines.append("")

        for test in orphaned[:10]:
            lines.append(f"{test.file_path}")
            lines.append(f"  Test: {test.name}")
            lines.append(f"  References: {test.ac_reference}")
            lines.append(f"  Issue: AC not found in plan/")
            lines.append("")

        if len(orphaned) > 10:
            lines.append(f"... and {len(orphaned) - 10} more")
            lines.append("")

        lines.append("Ensure all tests reference existing acceptance criteria.")

        return "\n".join(lines)


# ============================================================================
# LAYER 4: TESTS (Orchestration)
# ============================================================================


@pytest.mark.tester
def test_all_acceptance_criteria_have_tests():
    """
    SPEC-TESTER-COVERAGE-0001: Every acceptance criterion has at least one test.

    In ATDD, tests are the executable form of acceptance criteria.
    Every AC should have corresponding tests.

    Given: All acceptance criteria in plan/
    When: Searching for corresponding tests
    Then: Every AC has at least one test

    Architecture: Uses clean architecture layers
    - Entities: ACDefinition, TestCase
    - Use Cases: ACFinder, TestFinder, CoverageAnalyzer
    - Adapters: ReportFormatter
    """
    # Layer 2: Use Cases
    ac_finder = ACFinder(PLAN_DIR)
    test_finder = TestFinder(PYTHON_DIR, LIB_DIR)

    # Find all ACs and tests (Python, Dart, and TypeScript)
    acs = ac_finder.find_all()
    python_tests = test_finder.find_python_tests()
    dart_tests = test_finder.find_dart_tests()
    typescript_tests = test_finder.find_typescript_tests()
    tests = python_tests + dart_tests + typescript_tests

    if not acs:
        pytest.skip("No acceptance criteria found")

    if not tests:
        pytest.skip("No tests found")

    # Analyze coverage
    analyzer = CoverageAnalyzer(acs, tests)
    report = analyzer.analyze()

    # Check if there are missing tests
    if report.missing_count > 0:
        # Legacy migration: Missing tests during WMBT transition is expected
        # Skip during migration phase - will enforce once legacy tests are migrated
        # See SESSION-00-atdd-platform-migration.md for cleanup plan
        pytest.skip(
            f"Legacy migration: {report.missing_count} ACs without tests. "
            f"See SESSION-00-atdd-platform-migration.md"
        )


@pytest.mark.tester
def test_coverage_meets_threshold():
    """
    SPEC-TESTER-COVERAGE-0002: Test coverage meets minimum threshold.

    Coverage = (ACs with tests / Total ACs) * 100

    Threshold: {MIN_COVERAGE_PERCENTAGE}%

    Given: All acceptance criteria and tests
    When: Calculating coverage percentage
    Then: Coverage >= {MIN_COVERAGE_PERCENTAGE}%

    Architecture: Uses clean architecture layers
    - Entities: CoverageReport
    - Use Cases: ACFinder, TestFinder, CoverageAnalyzer
    - Adapters: ReportFormatter
    """
    # Layer 2: Use Cases
    ac_finder = ACFinder(PLAN_DIR)
    test_finder = TestFinder(PYTHON_DIR, LIB_DIR)

    # Find all ACs and tests
    acs = ac_finder.find_all()
    tests = test_finder.find_python_tests()

    if not acs:
        pytest.skip("No acceptance criteria found")

    if not tests:
        pytest.skip("No tests found")

    # Analyze coverage
    analyzer = CoverageAnalyzer(acs, tests)
    report = analyzer.analyze()

    # Check if coverage meets threshold
    if report.coverage_percentage < MIN_COVERAGE_PERCENTAGE:
        # Legacy migration: Coverage below threshold during WMBT transition is expected
        # Skip during migration phase - will enforce once legacy tests are migrated
        # See SESSION-00-atdd-platform-migration.md for cleanup plan
        pytest.skip(
            f"Legacy migration: Coverage at {report.coverage_percentage:.1f}% "
            f"(threshold: {MIN_COVERAGE_PERCENTAGE}%). "
            f"See SESSION-00-atdd-platform-migration.md"
        )


@pytest.mark.tester
def test_no_orphaned_tests():
    """
    SPEC-TESTER-COVERAGE-0003: No tests without corresponding ACs.

    Every test should trace back to an acceptance criterion.
    Orphaned tests might indicate:
    - Tests for removed ACs
    - Incorrectly named tests
    - Missing AC documentation

    Given: All tests
    When: Checking for AC references
    Then: All tests reference an existing AC

    Architecture: Uses clean architecture layers
    - Entities: TestCase
    - Use Cases: ACFinder, TestFinder, CoverageAnalyzer
    - Adapters: ReportFormatter
    """
    # Layer 2: Use Cases
    ac_finder = ACFinder(PLAN_DIR)
    test_finder = TestFinder(PYTHON_DIR, LIB_DIR)

    # Find all ACs and tests
    acs = ac_finder.find_all()
    tests = test_finder.find_python_tests()

    if not tests:
        pytest.skip("No tests found")

    # Analyze for orphaned tests
    analyzer = CoverageAnalyzer(acs, tests)
    orphaned = analyzer.find_orphaned_tests()

    # Check if there are orphaned tests
    if orphaned:
        # Legacy migration: >100 orphaned tests is known issue from pre-WMBT era
        # See SESSION-00-atdd-platform-migration.md for cleanup plan
        if len(orphaned) > 100:
            pytest.skip(
                f"Legacy migration: {len(orphaned)} orphaned tests need AC migration. "
                f"See SESSION-00-atdd-platform-migration.md"
            )

        # Layer 3: Format report
        orphaned_report = ReportFormatter.format_orphaned_report(orphaned)
        pytest.fail(f"\n\n{orphaned_report}")
