"""
Comprehensive API Endpoint Tests for ClaudeTools FastAPI Application

This test suite validates all 5 core API endpoints:
- Machines
- Clients
- Projects
- Sessions
- Tags

Tests include:
- API startup and health checks
- CRUD operations for all entities
- Authentication (with and without JWT tokens)
- Pagination parameters
- Error handling (404, 409, 422 responses)
"""

import sys
from datetime import timedelta
from uuid import uuid4

from fastapi.testclient import TestClient

# Import the FastAPI app and auth utilities
from api.main import app
from api.middleware.auth import create_access_token

# Create test client
client = TestClient(app)

# Test counters
tests_passed = 0
tests_failed = 0
test_results = []


def log_test(test_name: str, passed: bool, error_msg: str = ""):
    """Log test result and update counters."""
    global tests_passed, tests_failed
    if passed:
        tests_passed += 1
        status = "PASS"
        symbol = "[+]"
    else:
        tests_failed += 1
        status = "FAIL"
        symbol = "[-]"

    result = f"{symbol} {status}: {test_name}"
    if error_msg:
        result += f"\n  Error: {error_msg}"

    test_results.append((test_name, passed, error_msg))
    print(result)


def create_test_token():
    """Create a test JWT token for authentication."""
    token_data = {
        "sub": "test_user@claudetools.com",
        "scopes": ["msp:read", "msp:write", "msp:admin"]
    }
    return create_access_token(token_data, expires_delta=timedelta(hours=1))


def get_auth_headers():
    """Get authorization headers with test token."""
    token = create_test_token()
    return {"Authorization": f"Bearer {token}"}


# ============================================================================
# SECTION 1: API Health and Startup Tests
# ============================================================================

print("\n" + "="*70)
print("SECTION 1: API Health and Startup Tests")
print("="*70 + "\n")

def test_root_endpoint():
    """Test root endpoint returns API status."""
    try:
        response = client.get("/")
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"
        data = response.json()
        assert data["status"] == "online", f"Expected status 'online', got {data.get('status')}"
        assert "service" in data, "Response missing 'service' field"
        assert "version" in data, "Response missing 'version' field"
        log_test("Root endpoint (/)", True)
    except Exception as e:
        log_test("Root endpoint (/)", False, str(e))

def test_health_endpoint():
    """Test health check endpoint."""
    try:
        response = client.get("/health")
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"
        data = response.json()
        assert data["status"] == "healthy", f"Expected status 'healthy', got {data.get('status')}"
        log_test("Health check endpoint (/health)", True)
    except Exception as e:
        log_test("Health check endpoint (/health)", False, str(e))

def test_jwt_token_creation():
    """Test JWT token creation."""
    try:
        token = create_test_token()
        assert token is not None, "Token creation returned None"
        assert len(token) > 20, "Token seems too short"
        log_test("JWT token creation", True)
    except Exception as e:
        log_test("JWT token creation", False, str(e))


# ============================================================================
# SECTION 2: Authentication Tests
# ============================================================================

print("\n" + "="*70)
print("SECTION 2: Authentication Tests")
print("="*70 + "\n")

def test_unauthenticated_access():
    """Test that protected endpoints reject requests without auth."""
    try:
        response = client.get("/api/machines")
        # Can be 401 (Unauthorized) or 403 (Forbidden) depending on implementation
        assert response.status_code in [401, 403], f"Expected 401 or 403, got {response.status_code}"
        log_test("Unauthenticated access rejected", True)
    except Exception as e:
        log_test("Unauthenticated access rejected", False, str(e))

def test_authenticated_access():
    """Test that protected endpoints accept valid JWT tokens."""
    try:
        headers = get_auth_headers()
        response = client.get("/api/machines", headers=headers)
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"
        log_test("Authenticated access accepted", True)
    except Exception as e:
        log_test("Authenticated access accepted", False, str(e))

def test_invalid_token():
    """Test that invalid tokens are rejected."""
    try:
        headers = {"Authorization": "Bearer invalid_token_string"}
        response = client.get("/api/machines", headers=headers)
        assert response.status_code == 401, f"Expected 401, got {response.status_code}"
        log_test("Invalid token rejected", True)
    except Exception as e:
        log_test("Invalid token rejected", False, str(e))


# ============================================================================
# SECTION 3: Machine CRUD Operations
# ============================================================================

print("\n" + "="*70)
print("SECTION 3: Machine CRUD Operations")
print("="*70 + "\n")

machine_id = None

def test_create_machine():
    """Test creating a new machine."""
    global machine_id
    try:
        headers = get_auth_headers()
        machine_data = {
            "hostname": f"test-machine-{uuid4().hex[:8]}",
            "friendly_name": "Test Machine",
            "machine_type": "laptop",
            "platform": "win32",
            "is_active": True
        }
        response = client.post("/api/machines", json=machine_data, headers=headers)
        assert response.status_code == 201, f"Expected 201, got {response.status_code}. Response: {response.text}"
        data = response.json()
        assert "id" in data, f"Response missing 'id' field. Data: {data}"
        machine_id = data["id"]
        print(f"  Created machine with ID: {machine_id}")
        log_test("Create machine", True)
    except Exception as e:
        log_test("Create machine", False, str(e))

def test_list_machines():
    """Test listing machines with pagination."""
    try:
        headers = get_auth_headers()
        response = client.get("/api/machines?skip=0&limit=10", headers=headers)
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"
        data = response.json()
        assert "total" in data, "Response missing 'total' field"
        assert "machines" in data, "Response missing 'machines' field"
        assert isinstance(data["machines"], list), "machines field is not a list"
        log_test("List machines", True)
    except Exception as e:
        log_test("List machines", False, str(e))

def test_get_machine():
    """Test retrieving a specific machine by ID."""
    try:
        if machine_id is None:
            raise Exception("No machine_id available (create test may have failed)")
        headers = get_auth_headers()
        print(f"  Fetching machine with ID: {machine_id} (type: {type(machine_id)})")

        # List all machines to check if our machine exists
        list_response = client.get("/api/machines", headers=headers)
        all_machines = list_response.json().get("machines", [])
        print(f"  Total machines in DB: {len(all_machines)}")
        if all_machines:
            print(f"  First machine ID: {all_machines[0].get('id')} (type: {type(all_machines[0].get('id'))})")

        response = client.get(f"/api/machines/{machine_id}", headers=headers)
        assert response.status_code == 200, f"Expected 200, got {response.status_code}. Response: {response.text}"
        data = response.json()
        assert str(data["id"]) == str(machine_id), f"Expected ID {machine_id}, got {data.get('id')}"
        log_test("Get machine by ID", True)
    except Exception as e:
        log_test("Get machine by ID", False, str(e))

def test_update_machine():
    """Test updating a machine."""
    try:
        if machine_id is None:
            raise Exception("No machine_id available (create test may have failed)")
        headers = get_auth_headers()
        update_data = {
            "friendly_name": "Updated Test Machine",
            "notes": "Updated during testing"
        }
        response = client.put(f"/api/machines/{machine_id}", json=update_data, headers=headers)
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"
        data = response.json()
        assert data["friendly_name"] == "Updated Test Machine", "Update not reflected"
        log_test("Update machine", True)
    except Exception as e:
        log_test("Update machine", False, str(e))

def test_machine_not_found():
    """Test getting non-existent machine returns 404."""
    try:
        headers = get_auth_headers()
        fake_id = str(uuid4())
        response = client.get(f"/api/machines/{fake_id}", headers=headers)
        assert response.status_code == 404, f"Expected 404, got {response.status_code}"
        log_test("Machine not found (404)", True)
    except Exception as e:
        log_test("Machine not found (404)", False, str(e))

def test_delete_machine():
    """Test deleting a machine."""
    try:
        if machine_id is None:
            raise Exception("No machine_id available (create test may have failed)")
        headers = get_auth_headers()
        response = client.delete(f"/api/machines/{machine_id}", headers=headers)
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"
        log_test("Delete machine", True)
    except Exception as e:
        log_test("Delete machine", False, str(e))


# ============================================================================
# SECTION 4: Client CRUD Operations
# ============================================================================

print("\n" + "="*70)
print("SECTION 4: Client CRUD Operations")
print("="*70 + "\n")

client_id = None

def test_create_client():
    """Test creating a new client."""
    global client_id
    try:
        headers = get_auth_headers()
        client_data = {
            "name": f"Test Client {uuid4().hex[:8]}",
            "type": "msp_client",
            "primary_contact": "John Doe",
            "is_active": True
        }
        response = client.post("/api/clients", json=client_data, headers=headers)
        assert response.status_code == 201, f"Expected 201, got {response.status_code}. Response: {response.text}"
        data = response.json()
        assert "id" in data, f"Response missing 'id' field. Data: {data}"
        client_id = data["id"]
        print(f"  Created client with ID: {client_id}")
        log_test("Create client", True)
    except Exception as e:
        log_test("Create client", False, str(e))

def test_list_clients():
    """Test listing clients with pagination."""
    try:
        headers = get_auth_headers()
        response = client.get("/api/clients?skip=0&limit=10", headers=headers)
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"
        data = response.json()
        assert "total" in data, "Response missing 'total' field"
        assert "clients" in data, "Response missing 'clients' field"
        log_test("List clients", True)
    except Exception as e:
        log_test("List clients", False, str(e))

def test_get_client():
    """Test retrieving a specific client by ID."""
    try:
        if client_id is None:
            raise Exception("No client_id available")
        headers = get_auth_headers()
        response = client.get(f"/api/clients/{client_id}", headers=headers)
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"
        data = response.json()
        assert data["id"] == client_id, f"Expected ID {client_id}, got {data.get('id')}"
        log_test("Get client by ID", True)
    except Exception as e:
        log_test("Get client by ID", False, str(e))

def test_update_client():
    """Test updating a client."""
    try:
        if client_id is None:
            raise Exception("No client_id available")
        headers = get_auth_headers()
        update_data = {
            "primary_contact": "Jane Doe",
            "notes": "Updated contact"
        }
        response = client.put(f"/api/clients/{client_id}", json=update_data, headers=headers)
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"
        data = response.json()
        assert data["primary_contact"] == "Jane Doe", "Update not reflected"
        log_test("Update client", True)
    except Exception as e:
        log_test("Update client", False, str(e))

def test_delete_client():
    """Test deleting a client."""
    try:
        if client_id is None:
            raise Exception("No client_id available")
        headers = get_auth_headers()
        response = client.delete(f"/api/clients/{client_id}", headers=headers)
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"
        log_test("Delete client", True)
    except Exception as e:
        log_test("Delete client", False, str(e))


# ============================================================================
# SECTION 5: Project CRUD Operations
# ============================================================================

print("\n" + "="*70)
print("SECTION 5: Project CRUD Operations")
print("="*70 + "\n")

project_id = None
project_client_id = None

def test_create_project():
    """Test creating a new project."""
    global project_id, project_client_id
    try:
        headers = get_auth_headers()

        # First create a client for the project
        client_data = {
            "name": f"Project Test Client {uuid4().hex[:8]}",
            "type": "msp_client",
            "is_active": True
        }
        client_response = client.post("/api/clients", json=client_data, headers=headers)
        assert client_response.status_code == 201, f"Failed to create test client: {client_response.text}"
        project_client_id = client_response.json()["id"]

        # Now create the project
        project_data = {
            "name": f"Test Project {uuid4().hex[:8]}",
            "client_id": project_client_id,
            "status": "active"
        }
        response = client.post("/api/projects", json=project_data, headers=headers)
        assert response.status_code == 201, f"Expected 201, got {response.status_code}. Response: {response.text}"
        data = response.json()
        assert "id" in data, f"Response missing 'id' field. Data: {data}"
        project_id = data["id"]
        print(f"  Created project with ID: {project_id}")
        log_test("Create project", True)
    except Exception as e:
        log_test("Create project", False, str(e))

def test_list_projects():
    """Test listing projects with pagination."""
    try:
        headers = get_auth_headers()
        response = client.get("/api/projects?skip=0&limit=10", headers=headers)
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"
        data = response.json()
        assert "total" in data, "Response missing 'total' field"
        assert "projects" in data, "Response missing 'projects' field"
        log_test("List projects", True)
    except Exception as e:
        log_test("List projects", False, str(e))

def test_get_project():
    """Test retrieving a specific project by ID."""
    try:
        if project_id is None:
            raise Exception("No project_id available")
        headers = get_auth_headers()
        response = client.get(f"/api/projects/{project_id}", headers=headers)
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"
        data = response.json()
        assert data["id"] == project_id, f"Expected ID {project_id}, got {data.get('id')}"
        log_test("Get project by ID", True)
    except Exception as e:
        log_test("Get project by ID", False, str(e))

def test_update_project():
    """Test updating a project."""
    try:
        if project_id is None:
            raise Exception("No project_id available")
        headers = get_auth_headers()
        update_data = {
            "status": "completed",
            "notes": "Project completed during testing"
        }
        response = client.put(f"/api/projects/{project_id}", json=update_data, headers=headers)
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"
        data = response.json()
        assert data["status"] == "completed", "Update not reflected"
        log_test("Update project", True)
    except Exception as e:
        log_test("Update project", False, str(e))

def test_delete_project():
    """Test deleting a project."""
    try:
        if project_id is None:
            raise Exception("No project_id available")
        headers = get_auth_headers()
        response = client.delete(f"/api/projects/{project_id}", headers=headers)
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"

        # Clean up test client
        if project_client_id:
            client.delete(f"/api/clients/{project_client_id}", headers=headers)

        log_test("Delete project", True)
    except Exception as e:
        log_test("Delete project", False, str(e))


# ============================================================================
# SECTION 6: Session CRUD Operations
# ============================================================================

print("\n" + "="*70)
print("SECTION 6: Session CRUD Operations")
print("="*70 + "\n")

session_id = None
session_client_id = None
session_project_id = None

def test_create_session():
    """Test creating a new session."""
    global session_id, session_client_id, session_project_id
    try:
        headers = get_auth_headers()

        # Create client for session
        client_data = {
            "name": f"Session Test Client {uuid4().hex[:8]}",
            "type": "msp_client",
            "is_active": True
        }
        client_response = client.post("/api/clients", json=client_data, headers=headers)
        assert client_response.status_code == 201, f"Failed to create test client: {client_response.text}"
        session_client_id = client_response.json()["id"]

        # Create project for session
        project_data = {
            "name": f"Session Test Project {uuid4().hex[:8]}",
            "client_id": session_client_id,
            "status": "active"
        }
        project_response = client.post("/api/projects", json=project_data, headers=headers)
        assert project_response.status_code == 201, f"Failed to create test project: {project_response.text}"
        session_project_id = project_response.json()["id"]

        # Create session
        from datetime import date
        session_data = {
            "session_title": f"Test Session {uuid4().hex[:8]}",
            "session_date": str(date.today()),
            "client_id": session_client_id,
            "project_id": session_project_id,
            "status": "completed"
        }
        response = client.post("/api/sessions", json=session_data, headers=headers)
        assert response.status_code == 201, f"Expected 201, got {response.status_code}. Response: {response.text}"
        data = response.json()
        assert "id" in data, f"Response missing 'id' field. Data: {data}"
        session_id = data["id"]
        print(f"  Created session with ID: {session_id}")
        log_test("Create session", True)
    except Exception as e:
        log_test("Create session", False, str(e))

def test_list_sessions():
    """Test listing sessions with pagination."""
    try:
        headers = get_auth_headers()
        response = client.get("/api/sessions?skip=0&limit=10", headers=headers)
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"
        data = response.json()
        assert "total" in data, "Response missing 'total' field"
        assert "sessions" in data, "Response missing 'sessions' field"
        log_test("List sessions", True)
    except Exception as e:
        log_test("List sessions", False, str(e))

def test_get_session():
    """Test retrieving a specific session by ID."""
    try:
        if session_id is None:
            raise Exception("No session_id available")
        headers = get_auth_headers()
        response = client.get(f"/api/sessions/{session_id}", headers=headers)
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"
        data = response.json()
        assert data["id"] == session_id, f"Expected ID {session_id}, got {data.get('id')}"
        log_test("Get session by ID", True)
    except Exception as e:
        log_test("Get session by ID", False, str(e))

def test_update_session():
    """Test updating a session."""
    try:
        if session_id is None:
            raise Exception("No session_id available")
        headers = get_auth_headers()
        update_data = {
            "status": "completed",
            "summary": "Test session completed"
        }
        response = client.put(f"/api/sessions/{session_id}", json=update_data, headers=headers)
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"
        data = response.json()
        assert data["status"] == "completed", "Update not reflected"
        log_test("Update session", True)
    except Exception as e:
        log_test("Update session", False, str(e))

def test_delete_session():
    """Test deleting a session."""
    try:
        if session_id is None:
            raise Exception("No session_id available")
        headers = get_auth_headers()
        response = client.delete(f"/api/sessions/{session_id}", headers=headers)
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"

        # Clean up test data
        if session_project_id:
            client.delete(f"/api/projects/{session_project_id}", headers=headers)
        if session_client_id:
            client.delete(f"/api/clients/{session_client_id}", headers=headers)

        log_test("Delete session", True)
    except Exception as e:
        log_test("Delete session", False, str(e))


# ============================================================================
# SECTION 7: Tag CRUD Operations
# ============================================================================

print("\n" + "="*70)
print("SECTION 7: Tag CRUD Operations")
print("="*70 + "\n")

tag_id = None

def test_create_tag():
    """Test creating a new tag."""
    global tag_id
    try:
        headers = get_auth_headers()
        tag_data = {
            "name": f"test-tag-{uuid4().hex[:8]}",
            "category": "technology",
            "color": "#FF5733"
        }
        response = client.post("/api/tags", json=tag_data, headers=headers)
        assert response.status_code == 201, f"Expected 201, got {response.status_code}"
        data = response.json()
        assert "id" in data, "Response missing 'id' field"
        tag_id = data["id"]
        log_test("Create tag", True)
    except Exception as e:
        log_test("Create tag", False, str(e))

def test_list_tags():
    """Test listing tags with pagination."""
    try:
        headers = get_auth_headers()
        response = client.get("/api/tags?skip=0&limit=10", headers=headers)
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"
        data = response.json()
        assert "total" in data, "Response missing 'total' field"
        assert "tags" in data, "Response missing 'tags' field"
        log_test("List tags", True)
    except Exception as e:
        log_test("List tags", False, str(e))

def test_get_tag():
    """Test retrieving a specific tag by ID."""
    try:
        if tag_id is None:
            raise Exception("No tag_id available")
        headers = get_auth_headers()
        response = client.get(f"/api/tags/{tag_id}", headers=headers)
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"
        data = response.json()
        assert data["id"] == tag_id, f"Expected ID {tag_id}, got {data.get('id')}"
        log_test("Get tag by ID", True)
    except Exception as e:
        log_test("Get tag by ID", False, str(e))

def test_update_tag():
    """Test updating a tag."""
    try:
        if tag_id is None:
            raise Exception("No tag_id available")
        headers = get_auth_headers()
        update_data = {
            "color": "#00FF00",
            "description": "Updated test tag"
        }
        response = client.put(f"/api/tags/{tag_id}", json=update_data, headers=headers)
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"
        data = response.json()
        assert data["color"] == "#00FF00", "Update not reflected"
        log_test("Update tag", True)
    except Exception as e:
        log_test("Update tag", False, str(e))

def test_tag_duplicate_name():
    """Test creating tag with duplicate name returns 409."""
    try:
        if tag_id is None:
            raise Exception("No tag_id available")
        headers = get_auth_headers()

        # Get existing tag name
        existing_response = client.get(f"/api/tags/{tag_id}", headers=headers)
        existing_name = existing_response.json()["name"]

        # Try to create duplicate
        duplicate_data = {
            "name": existing_name,
            "category": "test"
        }
        response = client.post("/api/tags", json=duplicate_data, headers=headers)
        assert response.status_code == 409, f"Expected 409, got {response.status_code}"
        log_test("Tag duplicate name (409)", True)
    except Exception as e:
        log_test("Tag duplicate name (409)", False, str(e))

def test_delete_tag():
    """Test deleting a tag."""
    try:
        if tag_id is None:
            raise Exception("No tag_id available")
        headers = get_auth_headers()
        response = client.delete(f"/api/tags/{tag_id}", headers=headers)
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"
        log_test("Delete tag", True)
    except Exception as e:
        log_test("Delete tag", False, str(e))


# ============================================================================
# SECTION 8: Pagination Tests
# ============================================================================

print("\n" + "="*70)
print("SECTION 8: Pagination Tests")
print("="*70 + "\n")

def test_pagination_skip_limit():
    """Test pagination with skip and limit parameters."""
    try:
        headers = get_auth_headers()
        response = client.get("/api/machines?skip=0&limit=5", headers=headers)
        assert response.status_code == 200, f"Expected 200, got {response.status_code}"
        data = response.json()
        assert data["skip"] == 0, f"Expected skip=0, got {data.get('skip')}"
        assert data["limit"] == 5, f"Expected limit=5, got {data.get('limit')}"
        log_test("Pagination skip/limit parameters", True)
    except Exception as e:
        log_test("Pagination skip/limit parameters", False, str(e))

def test_pagination_max_limit():
    """Test that pagination enforces maximum limit."""
    try:
        headers = get_auth_headers()
        # Try to request more than max (1000)
        response = client.get("/api/machines?limit=2000", headers=headers)
        # Should either return 422 or clamp to max
        assert response.status_code in [200, 422], f"Unexpected status {response.status_code}"
        log_test("Pagination max limit enforcement", True)
    except Exception as e:
        log_test("Pagination max limit enforcement", False, str(e))


# ============================================================================
# Run All Tests
# ============================================================================

def run_all_tests():
    """Run all test functions."""
    print("\n" + "="*70)
    print("CLAUDETOOLS API ENDPOINT TESTS")
    print("="*70)

    # Section 1: Health
    test_root_endpoint()
    test_health_endpoint()
    test_jwt_token_creation()

    # Section 2: Auth
    test_unauthenticated_access()
    test_authenticated_access()
    test_invalid_token()

    # Section 3: Machines
    test_create_machine()
    test_list_machines()
    test_get_machine()
    test_update_machine()
    test_machine_not_found()
    test_delete_machine()

    # Section 4: Clients
    test_create_client()
    test_list_clients()
    test_get_client()
    test_update_client()
    test_delete_client()

    # Section 5: Projects
    test_create_project()
    test_list_projects()
    test_get_project()
    test_update_project()
    test_delete_project()

    # Section 6: Sessions
    test_create_session()
    test_list_sessions()
    test_get_session()
    test_update_session()
    test_delete_session()

    # Section 7: Tags
    test_create_tag()
    test_list_tags()
    test_get_tag()
    test_update_tag()
    test_tag_duplicate_name()
    test_delete_tag()

    # Section 8: Pagination
    test_pagination_skip_limit()
    test_pagination_max_limit()


if __name__ == "__main__":
    print("\n>> Starting ClaudeTools API Test Suite...")

    try:
        run_all_tests()

        # Print summary
        print("\n" + "="*70)
        print("TEST SUMMARY")
        print("="*70)
        print(f"\nTotal Tests: {tests_passed + tests_failed}")
        print(f"Passed: {tests_passed}")
        print(f"Failed: {tests_failed}")

        if tests_failed > 0:
            print("\nFAILED TESTS:")
            for name, passed, error in test_results:
                if not passed:
                    print(f"  - {name}")
                    if error:
                        print(f"    Error: {error}")

        if tests_failed == 0:
            print("\n>> All tests passed!")
            sys.exit(0)
        else:
            print(f"\n>> {tests_failed} test(s) failed")
            sys.exit(1)

    except Exception as e:
        print(f"\n>> Fatal error running tests: {e}")
        import traceback
        traceback.print_exc()
        sys.exit(1)
