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

from fmatch.saas.auth import get_current_account
from fmatch.saas.db import get_session
from fmatch.saas.models import (
    FieldMappingPolicy, User, UserRole, AuditEvent, ActionBatch, ActionStatus,
    FindingCategory, Integration
)
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.services.salesforce_gateway import SalesforceGateway
from fmatch.saas.services.roles import require_steward
from fmatch.saas.main import app

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

    class _AsyncTx:
        async def __aenter__(self):
            return None

        async def __aexit__(self, exc_type, exc, tb):
            return False

    session = Mock(spec=AsyncSession)
    session.begin = Mock(return_value=_AsyncTx())
    session.rollback = AsyncMock()
    session.merge = AsyncMock()
    return session

@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
    }


def _scalar_result(value):
    result = Mock()
    result.scalar_one_or_none.return_value = value
    return result

# 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.execute = AsyncMock(return_value=_scalar_result(None))
    mock_db_session.add = Mock()
    mock_db_session.commit = AsyncMock()
    mock_db_session.refresh = AsyncMock()
    service.validate_policy = AsyncMock(return_value=[])
    
    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_db_session.execute = AsyncMock(return_value=_scalar_result(existing_policy))
    mock_db_session.add = Mock()
    mock_db_session.commit = AsyncMock()
    mock_db_session.refresh = AsyncMock()
    service.validate_policy = AsyncMock(return_value=[])
    
    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.execute = AsyncMock(return_value=_scalar_result(None))
    mock_db_session.add = Mock()
    mock_db_session.commit = AsyncMock()
    mock_db_session.refresh = AsyncMock()
    service.validate_policy = AsyncMock(return_value=[])
    
    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.75  # Similarity should still be high enough for fuzzy 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()
    mock_sf_client.get_record = AsyncMock(return_value={"Account__c": "001456"})  # Current value matches
    mock_sf_client.update = AsyncMock()

    result = await service._rollback_field(
        mock_sf_client,
        {
            "object_type": "Lead",
            "record_id": "00Q123",
            "field": "Account__c",
            "before": None,
            "after": "001456",
        },
        AsyncMock(),  # save_callback
    )
    
    assert result["status"] == "rolled_back"
    mock_sf_client.update.assert_awaited_once_with(
        "Lead", "00Q123", {"Account__c": None}
    )

@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()
    mock_sf_client.get_record = AsyncMock(return_value={"Account__c": "001999"})  # Different value
    
    update_data = {
        "object_type": "Lead",
        "record_id": "00Q123",
        "field": "Account__c", 
        "before": None,
        "after": "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 = {
        "changes": [
            {
                "object_type": "Lead",
                "record_id": "00Q123",
                "field": "Account__c",
                "before": None,
                "after": "001456",
            }
        ]
    }

    integration = Mock(spec=Integration)
    integration.account_id = mock_user.account_id

    batch_result = Mock()
    batch_result.first.return_value = (mock_batch, mock_user.account_id)
    integration_result = Mock()
    integration_result.scalar_one_or_none.return_value = integration
    mock_db_session.execute = AsyncMock(side_effect=[batch_result, integration_result])
    mock_db_session.commit = AsyncMock()
    mock_db_session.merge = AsyncMock()
    mock_db_session.refresh = AsyncMock()
    
    with patch.object(SalesforceGateway, 'for_tenant', new=AsyncMock(return_value=Mock())):
        with patch.object(service, '_rollback_field', new=AsyncMock(return_value={"status": "rolled_back", "record_id": "00Q123", "field": "Account__c"})):
            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,
                    mock_user.account_id,
                    "Rolling back incorrect updates"
                )
                
                # Verify audit logging was called
                mock_audit_instance.log_action.assert_awaited_once()

# Integration Tests with FastAPI
def test_policy_api_endpoint():
    """Test policy API endpoint integration"""
    client = TestClient(app)
    mock_db = Mock(spec=AsyncSession)

    async def _override_session():
        yield mock_db

    app.dependency_overrides[get_session] = _override_session
    app.dependency_overrides[require_steward] = lambda: SimpleNamespace(
        id="user-1", account_id="acct-1", role=UserRole.STEWARD
    )
    try:
        with patch('fmatch.saas.api.v2.policies.PolicyService') as mock_service:
            with patch('fmatch.saas.api.v2.policies.AuditService') as mock_audit:
                mock_service_instance = Mock()
                mock_service_instance.create_or_update_policy = AsyncMock(return_value=SimpleNamespace(
                    id="policy-123",
                    version=1,
                    policy_hash="abc123",
                    salesforce_object="Lead",
                ))
                mock_service.return_value = mock_service_instance
                mock_audit_instance = Mock()
                mock_audit_instance.log_action = AsyncMock()
                mock_audit.return_value = mock_audit_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
    finally:
        app.dependency_overrides.clear()

def test_audit_api_endpoint():
    """Test audit API endpoint integration"""
    client = TestClient(app)
    mock_db = Mock(spec=AsyncSession)

    async def _override_session():
        yield mock_db

    app.dependency_overrides[get_session] = _override_session
    app.dependency_overrides[get_current_account] = lambda: "acct-1"
    app.dependency_overrides[require_steward] = lambda: SimpleNamespace(
        id="user-1", account_id="acct-1", role=UserRole.STEWARD
    )
    try:
        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
    finally:
        app.dependency_overrides.clear()

def test_rollback_api_endpoint():
    """Test rollback API endpoint integration"""
    client = TestClient(app)
    mock_db = Mock(spec=AsyncSession)

    async def _override_session():
        yield mock_db

    app.dependency_overrides[get_session] = _override_session
    app.dependency_overrides[get_current_account] = lambda: "acct-1"
    app.dependency_overrides[require_steward] = lambda: SimpleNamespace(
        id="user-1", account_id="acct-1", role=UserRole.STEWARD
    )
    try:
        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
    finally:
        app.dependency_overrides.clear()

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