import pytest
from unittest.mock import Mock, patch, AsyncMock
from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi.testclient import TestClient
from fastapi import HTTPException

from fmatch.saas.models import (
    FieldMappingPolicy, User, UserRole, AuditEvent, ActionBatch, ActionStatus,
    FindingCategory
)
from fmatch.saas.services.policy_service import PolicyService
from fmatch.saas.services.audit_service import AuditService
from fmatch.saas.services.rollback_service import RollbackService
from fmatch.saas.services.investigation_service import InvestigationService
from fmatch.saas.integrations.salesforce import SalesforceClient
from fmatch.saas.main import app

# Test fixtures
@pytest.fixture
def mock_db_session():
    """Mock database session"""
    return Mock(spec=AsyncSession)

@pytest.fixture
def mock_user():
    """Mock user with different roles"""
    user = Mock(spec=User)
    user.id = "test-user-123"
    user.email = "test@example.com"
    user.account_id = "test-account-123" 
    user.role = UserRole.STEWARD
    return user

@pytest.fixture
def mock_admin_user():
    """Mock admin user"""
    user = Mock(spec=User)
    user.id = "admin-user-123"
    user.email = "admin@example.com"
    user.account_id = "test-account-123"
    user.role = UserRole.ADMIN
    return user

@pytest.fixture
def mock_viewer_user():
    """Mock viewer user"""
    user = Mock(spec=User)
    user.id = "viewer-user-123"
    user.email = "viewer@example.com"
    user.account_id = "test-account-123"
    user.role = UserRole.VIEWER
    return user

@pytest.fixture
def sample_policy():
    """Sample field mapping policy"""
    return {
        "salesforce_object": "Lead",
        "matching_rules": {
            "primary_keys": ["Email", "Phone"],
            "matching_fields": [
                {"field": "Name", "weight": 0.4, "algorithm": "fuzzy", "enabled": True},
                {"field": "Website", "weight": 0.3, "algorithm": "domain", "enabled": True},
                {"field": "Phone", "weight": 0.3, "algorithm": "exact", "enabled": True}
            ],
            "blocking_field": "Domain__c",
            "blocking_key_length": 3
        },
        "writable_fields": ["Account__c"],
        "duplicate_threshold": 0.85,
        "link_threshold": 0.75
    }

# Policy Service Tests
@pytest.mark.asyncio
async def test_policy_creation(mock_db_session, mock_user, sample_policy):
    """Test field mapping policy creation and validation"""
    service = PolicyService(mock_db_session)
    
    # Mock database operations
    mock_db_session.add = Mock()
    mock_db_session.commit = AsyncMock()
    mock_db_session.refresh = AsyncMock()
    
    policy = await service.create_or_update_policy(
        sample_policy,
        mock_user.account_id,
        mock_user.id
    )
    
    assert policy.salesforce_object == "Lead"
    assert policy.version == 1
    assert policy.account_id == mock_user.account_id
    assert policy.created_by == mock_user.id
    mock_db_session.add.assert_called_once()
    mock_db_session.commit.assert_called_once()

@pytest.mark.asyncio
async def test_policy_validation(mock_db_session, mock_user):
    """Test policy validation against Salesforce schema"""
    service = PolicyService(mock_db_session)
    
    invalid_policy = {
        "salesforce_object": "InvalidObject",
        "matching_rules": {"invalid": "rules"},
        "duplicate_threshold": 1.5  # Invalid threshold
    }
    
    with pytest.raises(ValueError, match="Invalid threshold"):
        await service.create_or_update_policy(
            invalid_policy,
            mock_user.account_id,
            mock_user.id
        )

@pytest.mark.asyncio
async def test_policy_versioning(mock_db_session, mock_user, sample_policy):
    """Test policy versioning when updating existing policies"""
    service = PolicyService(mock_db_session)
    
    # Mock existing policy
    existing_policy = Mock(spec=FieldMappingPolicy)
    existing_policy.version = 1
    existing_policy.is_active = True
    
    mock_result = Mock()
    mock_result.scalar_one_or_none.return_value = existing_policy
    mock_db_session.execute.return_value = mock_result
    mock_db_session.add = Mock()
    mock_db_session.commit = AsyncMock()
    
    updated_policy = await service.create_or_update_policy(
        sample_policy,
        mock_user.account_id,
        mock_user.id
    )
    
    # Verify old policy was deactivated and new version created
    assert existing_policy.is_active == False
    mock_db_session.add.assert_called()

@pytest.mark.asyncio
async def test_policy_hash_generation(mock_db_session, mock_user, sample_policy):
    """Test policy hash generation for change detection"""
    service = PolicyService(mock_db_session)
    
    mock_db_session.add = Mock()
    mock_db_session.commit = AsyncMock()
    mock_db_session.refresh = AsyncMock()
    
    policy = await service.create_or_update_policy(
        sample_policy,
        mock_user.account_id,  
        mock_user.id
    )
    
    # Hash should be generated and consistent
    assert policy.policy_hash is not None
    assert len(policy.policy_hash) == 64  # SHA256 length

# RBAC Tests
@pytest.mark.asyncio
async def test_rbac_viewer_restrictions(mock_viewer_user):
    """Test viewer cannot apply actions"""
    from fmatch.saas.services.roles import require_steward
    
    with pytest.raises(HTTPException) as exc_info:
        await require_steward(mock_viewer_user)
    
    assert exc_info.value.status_code == 403
    assert "Insufficient permissions" in str(exc_info.value.detail)

@pytest.mark.asyncio 
async def test_rbac_steward_permissions(mock_user):
    """Test steward can apply actions"""
    from fmatch.saas.services.roles import require_steward
    
    # Should not raise an exception
    result = await require_steward(mock_user)
    assert result == mock_user

@pytest.mark.asyncio
async def test_rbac_admin_permissions(mock_admin_user):
    """Test admin can configure integrations"""
    from fmatch.saas.services.roles import require_admin
    
    # Should not raise an exception
    result = await require_admin(mock_admin_user)
    assert result == mock_admin_user

# Audit Logging Tests
@pytest.mark.asyncio
async def test_audit_action_logging(mock_db_session, mock_user):
    """Test audit event logging"""
    service = AuditService(mock_db_session)
    
    mock_db_session.add = Mock()
    mock_db_session.commit = AsyncMock()
    
    await service.log_action(
        actor=mock_user,
        action="field_update",
        object_type="Lead",
        record_ids=["00Q123456789ABC"],
        changes={
            "Lead:00Q123456789ABC": {
                "Account__c": {
                    "before": None,
                    "after": "001456789012DEF"
                }
            }
        },
        justification="Linking lead to correct account"
    )
    
    mock_db_session.add.assert_called_once()
    mock_db_session.commit.assert_called_once()

@pytest.mark.asyncio
async def test_audit_trail_query(mock_db_session, mock_user):
    """Test audit trail query with filters"""
    service = AuditService(mock_db_session)
    
    # Mock query result
    mock_events = [
        Mock(spec=AuditEvent, 
             id="1", 
             timestamp=datetime.utcnow(),
             action="field_update",
             actor_id=mock_user.id)
    ]
    
    mock_result = Mock()
    mock_result.scalars.return_value.all.return_value = mock_events
    mock_db_session.execute.return_value = mock_result
    
    events = await service.get_audit_trail(
        account_id=mock_user.account_id,
        action="field_update",
        limit=50
    )
    
    assert len(events) == 1
    assert events[0].action == "field_update"
    mock_db_session.execute.assert_called_once()

@pytest.mark.asyncio
async def test_audit_csv_export(mock_db_session):
    """Test CSV export functionality"""
    service = AuditService(mock_db_session)
    
    mock_events = [
        Mock(spec=AuditEvent,
             timestamp=datetime(2023, 1, 1, 12, 0, 0),
             actor_id="test-user",
             actor_role="steward", 
             action="field_update",
             object_type="Lead",
             record_ids=["123"],
             justification="Test action")
    ]
    
    csv_data = await service.export_audit_csv(mock_events)
    
    assert "timestamp,actor_id,actor_role,action" in csv_data
    assert "field_update" in csv_data
    assert "test-user" in csv_data

# Contact Deduplication Tests
@pytest.mark.asyncio
async def test_contact_email_deduplication(mock_db_session):
    """Test email-based contact deduplication"""
    service = InvestigationService(mock_db_session)
    
    contacts_data = [
        {"Id": "003001", "FirstName": "John", "LastName": "Smith", "Email": "john@company.com"},
        {"Id": "003002", "FirstName": "Johnny", "LastName": "Smith", "Email": "john@company.com"},  # Duplicate email
    ]
    
    findings = []
    stats = {"total_records": 0, "duplicates": 0, "link_candidates": 0}
    
    # Mock the paginate_soql to return our test data
    with patch.object(service, '_scan_contacts') as mock_scan:
        mock_scan.return_value = None  # We'll test the logic directly
        
        import pandas as pd
        df = pd.DataFrame(contacts_data)
        
        # Simulate duplicate detection logic
        processed = set()
        for i, contact1 in df.iterrows():
            if contact1['Id'] in processed:
                continue
            
            cluster = [contact1.to_dict()]
            for j, contact2 in df.iterrows():
                if i >= j or contact2['Id'] in processed:
                    continue
                
                if contact1.get('Email') == contact2.get('Email'):
                    cluster.append(contact2.to_dict())
                    processed.add(contact2['Id'])
            
            if len(cluster) > 1:
                findings.append({
                    "object_type": "Contact",
                    "category": FindingCategory.DUPLICATE,
                    "records": cluster
                })
                stats["duplicates"] += 1
        
        assert len(findings) == 1
        assert findings[0]["category"] == FindingCategory.DUPLICATE
        assert len(findings[0]["records"]) == 2

@pytest.mark.asyncio  
async def test_contact_name_account_matching(mock_db_session):
    """Test name + account matching for contacts"""
    service = InvestigationService(mock_db_session)
    
    from rapidfuzz import fuzz
    
    contact1 = {"FirstName": "John", "LastName": "Smith", "AccountId": "001123"}
    contact2 = {"FirstName": "Jon", "LastName": "Smyth", "AccountId": "001123"}  # Similar name, same account
    
    name1 = f"{contact1.get('FirstName', '')} {contact1.get('LastName', '')}"
    name2 = f"{contact2.get('FirstName', '')} {contact2.get('LastName', '')}"
    name_sim = fuzz.token_set_ratio(name1, name2) / 100.0
    
    assert name_sim > 0.85  # Should be considered a match
    assert contact1.get('AccountId') == contact2.get('AccountId')

@pytest.mark.asyncio
async def test_orphaned_contact_detection(mock_db_session):
    """Test orphaned contact detection and Account linkage"""
    service = InvestigationService(mock_db_session)
    
    orphaned_contact = {
        "Id": "003001", 
        "Email": "john@acme-corp.com",
        "AccountId": None
    }
    
    # Mock domain extraction
    email_domain = "acme-corp.com"
    
    # Should identify as a business domain (not personal)
    personal_domains = {
        'gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com'
    }
    is_personal = email_domain in personal_domains
    
    assert not is_personal
    assert orphaned_contact["AccountId"] is None
    assert "acme-corp.com" in orphaned_contact["Email"]

# Rollback Tests
@pytest.mark.asyncio
async def test_rollback_compare_and_set(mock_db_session, mock_user):
    """Test field update rollback with compare-and-set"""
    service = RollbackService(mock_db_session)
    
    # Mock action batch
    mock_batch = Mock(spec=ActionBatch)
    mock_batch.status = ActionStatus.APPLIED
    mock_batch.preview = {
        "field_updates": [{
            "object": "Lead",
            "id": "00Q123", 
            "field": "Account__c",
            "value": "001456"  # What we set it to
        }]
    }
    
    # Mock Salesforce client
    mock_sf_client = Mock(spec=SalesforceClient)
    mock_sf_client.get_record = AsyncMock(return_value={"Account__c": "001456"})  # Current value matches
    mock_sf_client.update_record = AsyncMock()
    
    mock_db_session.get.return_value = mock_batch
    
    with patch.object(service, '_get_salesforce_client', return_value=mock_sf_client):
        result = await service._rollback_field(
            mock_sf_client,
            mock_batch.preview["field_updates"][0],
            Mock()  # save_callback
        )
    
    assert result["status"] == "rolled_back"
    mock_sf_client.update_record.assert_called_once_with(
        "Lead", "00Q123", {"Account__c": None}, Mock()
    )

@pytest.mark.asyncio
async def test_rollback_skip_changed_values(mock_db_session, mock_user):
    """Test skipping rollback when values have changed"""
    service = RollbackService(mock_db_session)
    
    # Mock Salesforce client with changed value
    mock_sf_client = Mock(spec=SalesforceClient)
    mock_sf_client.get_record = AsyncMock(return_value={"Account__c": "001999"})  # Different value
    
    update_data = {
        "object": "Lead",
        "id": "00Q123",
        "field": "Account__c", 
        "value": "001456"  # Expected value
    }
    
    result = await service._rollback_field(
        mock_sf_client,
        update_data,
        Mock()  # save_callback
    )
    
    assert result["status"] == "skipped"
    assert "Field value has been modified" in result["reason"]

@pytest.mark.asyncio
async def test_rollback_audit_logging(mock_db_session, mock_user):
    """Test rollback audit logging"""
    service = RollbackService(mock_db_session)
    
    # Mock successful rollback
    mock_batch = Mock(spec=ActionBatch)
    mock_batch.status = ActionStatus.APPLIED
    mock_batch.preview = {"field_updates": []}
    
    mock_db_session.get.return_value = mock_batch
    mock_db_session.commit = AsyncMock()
    
    with patch.object(service, '_get_salesforce_client'):
        with patch.object(service, '_rollback_field', return_value={"status": "rolled_back"}):
            with patch('fmatch.saas.services.rollback_service.AuditService') as mock_audit:
                mock_audit_instance = Mock()
                mock_audit_instance.log_action = AsyncMock()
                mock_audit.return_value = mock_audit_instance
                
                await service.rollback_action_batch(
                    "batch-123",
                    mock_user,
                    "Rolling back incorrect updates"
                )
                
                # Verify audit logging was called
                mock_audit_instance.log_action.assert_called()

# Integration Tests with FastAPI
def test_policy_api_endpoint():
    """Test policy API endpoint integration"""
    client = TestClient(app)
    
    with patch('fmatch.saas.api.v2.policies.get_current_account'):
        with patch('fmatch.saas.api.v2.policies.require_steward'):
            with patch('fmatch.saas.api.v2.policies.PolicyService') as mock_service:
                mock_service_instance = Mock()
                mock_service_instance.create_or_update_policy = AsyncMock(return_value=Mock(
                    id="policy-123",
                    version=1,
                    policy_hash="abc123"
                ))
                mock_service.return_value = mock_service_instance
                
                response = client.post("/api/v2/policies", json={
                    "salesforce_object": "Lead",
                    "matching_rules": {},
                    "writable_fields": [],
                    "duplicate_threshold": 0.85,
                    "link_threshold": 0.75
                })
                
                assert response.status_code == 200

def test_audit_api_endpoint():
    """Test audit API endpoint integration"""
    client = TestClient(app)
    
    with patch('fmatch.saas.api.v2.audit.get_current_account'):
        with patch('fmatch.saas.api.v2.audit.require_steward'):
            with patch('fmatch.saas.api.v2.audit.AuditService') as mock_service:
                mock_service_instance = Mock()
                mock_service_instance.get_audit_trail = AsyncMock(return_value=[])
                mock_service.return_value = mock_service_instance
                
                response = client.get("/api/v2/audit/events")
                assert response.status_code == 200

def test_rollback_api_endpoint():
    """Test rollback API endpoint integration"""
    client = TestClient(app)
    
    with patch('fmatch.saas.api.v2.rollback.get_current_account'):
        with patch('fmatch.saas.api.v2.rollback.require_steward'):
            with patch('fmatch.saas.api.v2.rollback.RollbackService') as mock_service:
                mock_service_instance = Mock()
                mock_service_instance.rollback_action_batch = AsyncMock(return_value={
                    "rolled_back": [],
                    "skipped": [],
                    "errors": []
                })
                mock_service.return_value = mock_service_instance
                
                response = client.post("/api/v2/rollback", json={
                    "action_batch_id": "batch-123",
                    "justification": "Test rollback"
                })
                
                assert response.status_code == 200

if __name__ == "__main__":
    pytest.main([__file__, "-v"])