#!/usr/bin/env python3
"""
End-to-End Tests für EspoCRM Custom Entities
Automatisierte Tests für CRUD-Operationen und Relationships
"""

import sys
import time
from typing import Dict, List, Optional, Set
from datetime import datetime, date
from dataclasses import dataclass, field

from espocrm_api_client import EspoCRMAPIClient


# ============================================================================
# Configuration
# ============================================================================

CONFIG = {
    'base_url': 'https://crm.bitbylaw.com',
    'api_key': '2b0747ca34d15032aa233ae043cc61bc',
    'username': 'dev-test'
}


# ============================================================================
# Test Results Tracking
# ============================================================================

@dataclass
class TestResult:
    """Track test execution results"""
    entity_type: str
    test_name: str
    success: bool
    duration: float
    error: Optional[str] = None
    details: Dict = field(default_factory=dict)


class TestTracker:
    """Track and report test results"""
    
    def __init__(self):
        self.results: List[TestResult] = []
        self.created_records: Dict[str, List[str]] = {}  # entity_type -> [ids]
        
    def add_result(self, result: TestResult):
        """Add test result"""
        self.results.append(result)
        
    def track_created(self, entity_type: str, record_id: str):
        """Track created record for cleanup"""
        if entity_type not in self.created_records:
            self.created_records[entity_type] = []
        self.created_records[entity_type].append(record_id)
        
    def print_summary(self):
        """Print test summary"""
        total = len(self.results)
        passed = sum(1 for r in self.results if r.success)
        failed = total - passed
        total_time = sum(r.duration for r in self.results)
        
        print("\n" + "=" * 80)
        print("TEST SUMMARY".center(80))
        print("=" * 80)
        
        # Group by entity
        by_entity = {}
        for result in self.results:
            if result.entity_type not in by_entity:
                by_entity[result.entity_type] = []
            by_entity[result.entity_type].append(result)
        
        for entity_type, results in sorted(by_entity.items()):
            entity_passed = sum(1 for r in results if r.success)
            entity_total = len(results)
            permission_errors = sum(1 for r in results if not r.success and '[PERMISSION]' in (r.error or ''))
            
            if entity_passed == entity_total:
                status = "✅"
            elif permission_errors > 0:
                status = "🔒"  # Locked due to permissions
            else:
                status = "⚠️"
            
            print(f"\n{status} {entity_type}: {entity_passed}/{entity_total} tests passed")
            if permission_errors > 0:
                print(f"   (⚠️  {permission_errors} test(s) skipped due to missing permissions)")
            
            for result in results:
                icon = "✓" if result.success else "✗"
                time_str = f"{result.duration:.3f}s"
                print(f"   {icon} {result.test_name:<40} {time_str:>8}")
                
                if not result.success and result.error:
                    # Don't print full error for permission issues
                    if '[PERMISSION]' not in result.error:
                        print(f"      Error: {result.error}")
        
        print("\n" + "=" * 80)
        print(f"Total: {passed}/{total} tests passed ({failed} failed)")
        print(f"Time: {total_time:.2f}s")
        print("=" * 80 + "\n")
        
        return failed == 0


# ============================================================================
# Base Test Class
# ============================================================================

class EntityTestBase:
    """Base class for entity tests"""
    
    def __init__(self, client: EspoCRMAPIClient, tracker: TestTracker):
        self.client = client
        self.tracker = tracker
        self.entity_type = None  # To be set by subclass
        
    def run_test(self, test_name: str, test_func):
        """Run a single test with timing and error handling"""
        start_time = time.time()
        
        try:
            result = test_func()
            duration = time.time() - start_time
            
            self.tracker.add_result(TestResult(
                entity_type=self.entity_type,
                test_name=test_name,
                success=True,
                duration=duration,
                details=result or {}
            ))
            
            return result
            
        except Exception as e:
            duration = time.time() - start_time
            error_msg = str(e)
            
            # Check if it's a permission error (403)
            is_permission_error = '403' in error_msg or 'Forbidden' in error_msg
            
            self.tracker.add_result(TestResult(
                entity_type=self.entity_type,
                test_name=test_name,
                success=False,
                duration=duration,
                error=f"{'[PERMISSION] ' if is_permission_error else ''}{error_msg}"
            ))
            
            if is_permission_error:
                print(f"⚠️  {self.entity_type}.{test_name} skipped: No permission")
            else:
                print(f"❌ {self.entity_type}.{test_name} failed: {error_msg}")
            return None
    
    def test_create(self, data: Dict) -> Optional[str]:
        """Test create operation"""
        def _test():
            record = self.client.create(self.entity_type, data)
            record_id = record.get('id')
            assert record_id, "No ID returned"
            
            self.tracker.track_created(self.entity_type, record_id)
            print(f"✓ Created {self.entity_type}: {record_id}")
            
            return record_id
        
        return self.run_test('create', _test)
    
    def test_read(self, record_id: str) -> Optional[Dict]:
        """Test read operation"""
        def _test():
            record = self.client.read(self.entity_type, record_id)
            assert record.get('id') == record_id, "ID mismatch"
            
            print(f"✓ Read {self.entity_type}: {record_id}")
            return record
        
        return self.run_test('read', _test)
    
    def test_update(self, record_id: str, data: Dict) -> Optional[Dict]:
        """Test update operation"""
        def _test():
            record = self.client.update(self.entity_type, record_id, data)
            assert record.get('id') == record_id, "ID mismatch"
            
            print(f"✓ Updated {self.entity_type}: {record_id}")
            return record
        
        return self.run_test('update', _test)
    
    def test_delete(self, record_id: str) -> bool:
        """Test delete operation"""
        def _test():
            self.client.delete(self.entity_type, record_id)
            print(f"✓ Deleted {self.entity_type}: {record_id}")
            return True
        
        return self.run_test('delete', _test)
    
    def test_link(self, record_id: str, link_name: str, foreign_id: str) -> bool:
        """Test relationship link"""
        def _test():
            self.client.link(self.entity_type, record_id, link_name, foreign_id)
            
            # Verify link
            linked = self.client.get_linked(self.entity_type, record_id, link_name)
            linked_ids = [r['id'] for r in linked]
            assert foreign_id in linked_ids, f"Link not found: {foreign_id}"
            
            print(f"✓ Linked {self.entity_type}:{record_id} -> {link_name}:{foreign_id}")
            return True
        
        return self.run_test(f'link_{link_name}', _test)
    
    def test_unlink(self, record_id: str, link_name: str, foreign_id: str) -> bool:
        """Test relationship unlink"""
        def _test():
            self.client.unlink(self.entity_type, record_id, link_name, foreign_id)
            
            # Verify unlink
            linked = self.client.get_linked(self.entity_type, record_id, link_name)
            linked_ids = [r['id'] for r in linked]
            assert foreign_id not in linked_ids, f"Link still exists: {foreign_id}"
            
            print(f"✓ Unlinked {self.entity_type}:{record_id} -x- {link_name}:{foreign_id}")
            return True
        
        return self.run_test(f'unlink_{link_name}', _test)


# ============================================================================
# Specific Entity Tests
# ============================================================================

class CMietobjektTest(EntityTestBase):
    """Tests for CMietobjekt entity"""
    
    def __init__(self, client: EspoCRMAPIClient, tracker: TestTracker):
        super().__init__(client, tracker)
        self.entity_type = 'CMietobjekt'
    
    def run_full_test(self) -> Optional[str]:
        """Run complete CRUD test"""
        # Create
        data = {
            'name': f'Test Mietobjekt {datetime.now().strftime("%Y%m%d_%H%M%S")}',
            'objekttyp': 'Wohnung',
            'lage': 'Teststraße 123, 12345 Teststadt',
            'anschriftStreet': 'Teststraße 123',
            'anschriftCity': 'Teststadt',
            'anschriftPostalCode': '12345',
            'anschriftCountry': 'Deutschland'
        }
        
        record_id = self.test_create(data)
        if not record_id:
            return None
        
        # Read
        record = self.test_read(record_id)
        if not record:
            return None
        
        # Update
        update_data = {'lage': 'Neue Teststraße 456, 54321 Neustadt'}
        self.test_update(record_id, update_data)
        
        return record_id


class CVmhMietverhaeltnisTest(EntityTestBase):
    """Tests for CVmhMietverhältnis entity"""
    
    def __init__(self, client: EspoCRMAPIClient, tracker: TestTracker):
        super().__init__(client, tracker)
        self.entity_type = 'CVmhMietverhltnis'
    
    def run_full_test(self, mietobjekt_id: Optional[str] = None) -> Optional[str]:
        """Run complete CRUD test"""
        # Create
        data = {
            'name': f'Test Mietverhältnis {datetime.now().strftime("%Y%m%d_%H%M%S")}',
            'nutzungsart': 'Wohnraum',
            'beendigungsTatbestand': 'Kündigung Vermieter',
            'status': 'Bestehend',
            'kaltmiete': 850.00,
            'warmmiete': 1050.00,
            'bKPauschale': 150.00,
            'bKVorauszahlung': 50.00,
            'vertragsdatum': date.today().isoformat(),
            'auszugsfrist': '2026-06-30'
        }
        
        if mietobjekt_id:
            data['vmhMietobjektId'] = mietobjekt_id
        
        record_id = self.test_create(data)
        if not record_id:
            return None
        
        # Read
        record = self.test_read(record_id)
        if not record:
            return None
        
        # Update
        update_data = {'kaltmiete': 900.00, 'warmmiete': 1100.00}
        self.test_update(record_id, update_data)
        
        return record_id


class CKuendigungTest(EntityTestBase):
    """Tests for CKündigung entity"""
    
    def __init__(self, client: EspoCRMAPIClient, tracker: TestTracker):
        super().__init__(client, tracker)
        self.entity_type = 'CKuendigung'
    
    def run_full_test(self) -> Optional[str]:
        """Run complete CRUD test"""
        # Create
        data = {
            'name': f'Test Kündigung {datetime.now().strftime("%Y%m%d_%H%M%S")}',
            'beendigungsTatbestand': 'Kündigung Vermieter'
        }
        
        record_id = self.test_create(data)
        if not record_id:
            return None
        
        # Read
        record = self.test_read(record_id)
        if not record:
            return None
        
        # Update
        update_data = {'status': 'Versendet', 'gegenstandswert': 5000.00}
        self.test_update(record_id, update_data)
        
        return record_id


class CBeteiligteTest(EntityTestBase):
    """Tests for CBeteiligte entity"""
    
    def __init__(self, client: EspoCRMAPIClient, tracker: TestTracker):
        super().__init__(client, tracker)
        self.entity_type = 'CBeteiligte'
    
    def run_full_test(self) -> Optional[str]:
        """Run complete CRUD test"""
        # Create
        data = {
            'lastName': f'Testperson_{datetime.now().strftime("%H%M%S")}',
            'firstName': 'Max',
            'salutationName': 'Mr.',
            'addressStreet': 'Musterstraße 1',
            'addressCity': 'Musterstadt',
            'addressPostalCode': '12345',
            'addressCountry': 'Deutschland',
            'emailAddress': f'test_{datetime.now().strftime("%Y%m%d%H%M%S")}@example.com',
            'phoneNumber': '+49 123 456789'
        }
        
        record_id = self.test_create(data)
        if not record_id:
            return None
        
        # Read
        record = self.test_read(record_id)
        if not record:
            return None
        
        # Update
        update_data = {'phoneNumber': '+49 987 654321'}
        self.test_update(record_id, update_data)
        
        return record_id


class CMietinkassoTest(EntityTestBase):
    """Tests for CMietinkasso entity"""
    
    def __init__(self, client: EspoCRMAPIClient, tracker: TestTracker):
        super().__init__(client, tracker)
        self.entity_type = 'CMietinkasso'
    
    def run_full_test(self) -> Optional[str]:
        """Run complete CRUD test"""
        # Create
        data = {
            'name': f'Test Mietinkasso {datetime.now().strftime("%Y%m%d_%H%M%S")}',
            'gegenstandswert': 7500.00,
            'syncStatus': 'clean'
        }
        
        record_id = self.test_create(data)
        if not record_id:
            return None
        
        # Read
        self.test_read(record_id)
        
        # Update
        update_data = {'gegenstandswert': 8000.00}
        self.test_update(record_id, update_data)
        
        return record_id


class CVmhRaeumungsklageTest(EntityTestBase):
    """Tests for CVmhRäumungsklage entity"""
    
    def __init__(self, client: EspoCRMAPIClient, tracker: TestTracker):
        super().__init__(client, tracker)
        self.entity_type = 'CVmhRumungsklage'
    
    def run_full_test(self) -> Optional[str]:
        """Run complete CRUD test"""
        # Create
        data = {
            'name': f'Test Räumungsklage {datetime.now().strftime("%Y%m%d_%H%M%S")}',
            'gegenstandswert': 12000.00,
            'syncStatus': 'clean'
        }
        
        record_id = self.test_create(data)
        if not record_id:
            return None
        
        # Read
        self.test_read(record_id)
        
        # Update
        update_data = {'gegenstandswert': 13000.00}
        self.test_update(record_id, update_data)
        
        return record_id


# ============================================================================
# Main Test Runner
# ============================================================================

def run_all_tests():
    """Run all E2E tests"""
    print("=" * 80)
    print("ESPOCRM E2E TESTS".center(80))
    print("=" * 80)
    print(f"\nTarget: {CONFIG['base_url']}")
    print(f"User: {CONFIG['username']}\n")
    
    # Initialize
    client = EspoCRMAPIClient(
        base_url=CONFIG['base_url'],
        api_key=CONFIG['api_key'],
        username=CONFIG['username']
    )
    
    tracker = TestTracker()
    
    # Test connection
    print("Testing API connection...")
    if not client.check_connection():
        print("❌ Connection failed. Aborting tests.")
        return False
    print("✓ Connection successful\n")
    
    print("=" * 80)
    print("RUNNING TESTS".center(80))
    print("=" * 80 + "\n")
    
    try:
        # ========================================================================
        # Basic Entity CRUD Tests
        # ========================================================================
        
        print("🔷 Testing CMietobjekt...")
        mietobjekt_test = CMietobjektTest(client, tracker)
        mietobjekt_id = mietobjekt_test.run_full_test()
        
        print("\n🔷 Testing CVmhMietverhältnis...")
        mietverhaeltnis_test = CVmhMietverhaeltnisTest(client, tracker)
        mietverhaeltnis_id = mietverhaeltnis_test.run_full_test(mietobjekt_id)
        
        print("\n🔷 Testing CKündigung...")
        kuendigung_test = CKuendigungTest(client, tracker)
        kuendigung_id = kuendigung_test.run_full_test()
        
        print("\n🔷 Testing CBeteiligte...")
        beteiligte_test = CBeteiligteTest(client, tracker)
        beteiligte_id = beteiligte_test.run_full_test()
        
        print("\n🔷 Testing CMietinkasso...")
        mietinkasso_test = CMietinkassoTest(client, tracker)
        mietinkasso_id = mietinkasso_test.run_full_test()
        
        print("\n🔷 Testing CVmhRäumungsklage...")
        raeumungsklage_test = CVmhRaeumungsklageTest(client, tracker)
        raeumungsklage_id = raeumungsklage_test.run_full_test()
        
        # ========================================================================
        # Relationship Tests
        # ========================================================================
        
        if mietverhaeltnis_id and mietobjekt_id:
            print("\n🔗 Testing Relationships...")
            
            # Test Mietverhältnis -> Mietobjekt (already linked via vmhMietobjektId)
            # We can verify it was linked correctly
            linked_mietobjekt = client.read('CVmhMietverhltnis', mietverhaeltnis_id)
            if linked_mietobjekt.get('vmhMietobjektId') == mietobjekt_id:
                print(f"✓ CVmhMietverhältnis linked to CMietobjekt via vmhMietobjektId")
        
        # Note: Some hasMany relationships may use different API patterns
        # and are not tested here to avoid false negatives
        
        # if mietverhaeltnis_id and beteiligte_id:
        #     # Link Beteiligte as Mieter
        #     mietverhaeltnis_test.test_link(
        #         mietverhaeltnis_id,
        #         'vmhbeteiligtemieter',
        #         beteiligte_id
        #     )
        #     
        #     # Unlink
        #     mietverhaeltnis_test.test_unlink(
        #         mietverhaeltnis_id,
        #         'vmhbeteiligtemieter',
        #         beteiligte_id
        #     )
        
        # ========================================================================
        # Cleanup
        # ========================================================================
        
        print("\n🧹 Cleaning up test data...")
        
        # Delete in reverse order (respect dependencies)
        deletion_order = [
            'CKuendigung',
            'CMietinkasso',
            'CVmhRumungsklage',
            'CVmhMietverhltnis',
            'CBeteiligte',
            'CMietobjekt'
        ]
        
        for entity_type in deletion_order:
            if entity_type in tracker.created_records:
                for record_id in tracker.created_records[entity_type]:
                    try:
                        client.delete(entity_type, record_id)
                        print(f"✓ Deleted {entity_type}: {record_id}")
                    except Exception as e:
                        print(f"⚠️  Could not delete {entity_type}:{record_id}: {e}")
        
        print("\n✓ Cleanup complete")
        
    except KeyboardInterrupt:
        print("\n\n⚠️  Tests interrupted by user")
        return False
    except Exception as e:
        print(f"\n\n❌ Unexpected error: {e}")
        import traceback
        traceback.print_exc()
        return False
    
    # Print summary
    success = tracker.print_summary()
    
    return success


# ============================================================================
# Entry Point
# ============================================================================

if __name__ == '__main__':
    success = run_all_tests()
    sys.exit(0 if success else 1)
