"""
Contract validation tests using pytest for regression prevention.
These tests validate API responses match expected schemas and value ranges.
"""
import pytest
from typing import Dict, Any
from fastapi.testclient import TestClient
import sys
import os

# Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'src'))


def validate_score_range(score: float, field_name: str = "score") -> None:
    """Validate score is in 0-1 range."""
    assert score is not None, f"{field_name} must not be None"
    assert 0 <= score <= 1, f"{field_name} must be between 0 and 1, got {score}"


def validate_l2a_suggestion(suggestion: Dict[str, Any]) -> None:
    """Validate L2A suggestion structure."""
    # Required fields
    assert "lead_id" in suggestion
    assert "account_id" in suggestion
    assert "score" in suggestion
    assert "tier" in suggestion
    assert "status" in suggestion
    
    # Score validation
    validate_score_range(suggestion["score"])
    
    # Evidence strength if present
    if "evidence_strength" in suggestion:
        validate_score_range(suggestion["evidence_strength"], "evidence_strength")
    
    # Field score details validation
    if "field_score_details" in suggestion and suggestion["field_score_details"]:
        if isinstance(suggestion["field_score_details"], list):
            for detail in suggestion["field_score_details"]:
                if "score" in detail:
                    score = detail["score"]
                    # All scores should be normalized to 0-1 now
                    validate_score_range(score, "field_score_details.score")
    
    # Tier validation
    valid_tiers = ["CERTAIN", "LIKELY", "POSSIBLE", "REVIEW", "UNLIKELY"]
    assert suggestion["tier"] in valid_tiers, f"Invalid tier: {suggestion['tier']}"
    
    # Status validation
    valid_statuses = ["pending", "linked", "rejected", "error"]
    assert suggestion["status"] in valid_statuses, f"Invalid status: {suggestion['status']}"


def validate_dedupe_suggestion(suggestion: Dict[str, Any]) -> None:
    """Validate dedupe suggestion structure."""
    # Required fields
    assert "rec_id_1" in suggestion
    assert "rec_id_2" in suggestion
    assert "score" in suggestion
    assert "status" in suggestion
    
    # Score validation
    validate_score_range(suggestion["score"])
    
    # Ensure no self-matches
    assert suggestion["rec_id_1"] != suggestion["rec_id_2"], "Self-match detected"
    
    # Ensure canonical ordering (rec_id_1 < rec_id_2)
    if isinstance(suggestion["rec_id_1"], str) and isinstance(suggestion["rec_id_2"], str):
        assert suggestion["rec_id_1"] < suggestion["rec_id_2"], \
            f"Non-canonical ordering: {suggestion['rec_id_1']} >= {suggestion['rec_id_2']}"
    
    # Evidence strength if present
    if "evidence_strength" in suggestion:
        validate_score_range(suggestion["evidence_strength"], "evidence_strength")
    
    # Field score details validation
    if "field_score_details" in suggestion and suggestion["field_score_details"]:
        if isinstance(suggestion["field_score_details"], list):
            for detail in suggestion["field_score_details"]:
                if "score" in detail:
                    score = detail["score"]
                    # All scores should be normalized to 0-1 now
                    validate_score_range(score, "field_score_details.score")


def validate_merge_plan_response(response: Dict[str, Any]) -> None:
    """Validate merge-plan endpoint response."""
    # Required fields
    assert "cluster_id" in response
    assert "object_type" in response
    assert "primary_id" in response  # Not recommended_primary_id
    assert "fields" in response
    assert "candidates" in response
    
    # Validate fields structure (FieldValue schema)
    for field in response["fields"]:
        assert "field" in field, "Field must have 'field' key (not 'api')"
        assert "values" in field, "Field must have 'values' dict"
        assert "label" in field
        assert "group" in field
        assert "hasConflict" in field
        assert "locked" in field
        
        # Values must be a dict mapping record_id -> value
        assert isinstance(field["values"], dict), "values must be a dict, not a list"
    
    # Validate candidates
    for candidate in response["candidates"]:
        assert "id" in candidate
        assert "score" in candidate
        validate_score_range(candidate["score"])
        
        if "tier" in candidate:
            valid_tiers = ["CERTAIN", "HIGH", "MEDIUM", "POSSIBLE"]
            assert candidate["tier"] in valid_tiers
    
    # Validate conflict count
    if "conflict_count" in response:
        actual_conflicts = sum(1 for f in response["fields"] if f.get("hasConflict"))
        assert response["conflict_count"] == actual_conflicts, \
            f"conflict_count mismatch: reported {response['conflict_count']}, actual {actual_conflicts}"


class TestL2AContracts:
    """Test L2A API contracts."""
    
    def test_l2a_suggestion_valid(self):
        """Test valid L2A suggestion passes validation."""
        suggestion = {
            "lead_id": "00Q123",
            "account_id": "001456",
            "score": 0.85,
            "tier": "LIKELY",
            "status": "pending",
            "evidence_strength": 0.75,
            "field_score_details": [
                {"source_field": "email", "score": 0.95},
                {"source_field": "company", "score": 0.75}  # Already normalized
            ]
        }
        validate_l2a_suggestion(suggestion)
    
    def test_l2a_suggestion_invalid_score(self):
        """Test L2A suggestion with invalid score fails."""
        suggestion = {
            "lead_id": "00Q123",
            "account_id": "001456",
            "score": 1.5,  # Invalid
            "tier": "LIKELY",
            "status": "pending"
        }
        with pytest.raises(AssertionError, match="score must be between 0 and 1"):
            validate_l2a_suggestion(suggestion)
    
    def test_l2a_suggestion_invalid_tier(self):
        """Test L2A suggestion with invalid tier fails."""
        suggestion = {
            "lead_id": "00Q123",
            "account_id": "001456",
            "score": 0.85,
            "tier": "MAYBE",  # Invalid
            "status": "pending"
        }
        with pytest.raises(AssertionError, match="Invalid tier"):
            validate_l2a_suggestion(suggestion)
    
    def test_l2a_field_scores_normalized(self):
        """Test that field scores are properly normalized to 0-1."""
        suggestion = {
            "lead_id": "00Q123",
            "account_id": "001456",
            "score": 0.85,
            "tier": "LIKELY",
            "status": "pending",
            "field_score_details": [
                {"source_field": "email", "score": 0.95},
                {"source_field": "company", "score": 0.80},
                {"source_field": "phone", "score": 0.65}
            ]
        }
        validate_l2a_suggestion(suggestion)
        
        # All scores should be 0-1
        for detail in suggestion["field_score_details"]:
            assert 0 <= detail["score"] <= 1


class TestDedupeContracts:
    """Test dedupe API contracts."""
    
    def test_dedupe_suggestion_valid(self):
        """Test valid dedupe suggestion passes validation."""
        suggestion = {
            "rec_id_1": "001123",
            "rec_id_2": "001456",
            "score": 0.75,
            "status": "pending",
            "evidence_strength": 0.65,
            "field_score_details": [
                {"source_field": "name", "score": 0.85},
                {"source_field": "phone", "score": 0.90}
            ]
        }
        validate_dedupe_suggestion(suggestion)
    
    def test_dedupe_self_match_fails(self):
        """Test dedupe with self-match fails."""
        suggestion = {
            "rec_id_1": "001123",
            "rec_id_2": "001123",  # Self-match
            "score": 1.0,
            "status": "pending"
        }
        with pytest.raises(AssertionError, match="Self-match detected"):
            validate_dedupe_suggestion(suggestion)
    
    def test_dedupe_non_canonical_fails(self):
        """Test dedupe with non-canonical ordering fails."""
        suggestion = {
            "rec_id_1": "001456",  # Should be second
            "rec_id_2": "001123",  # Should be first
            "score": 0.75,
            "status": "pending"
        }
        with pytest.raises(AssertionError, match="Non-canonical ordering"):
            validate_dedupe_suggestion(suggestion)
    
    def test_dedupe_invalid_score_range(self):
        """Test dedupe with out-of-range score fails."""
        suggestion = {
            "rec_id_1": "001123",
            "rec_id_2": "001456",
            "score": -0.1,  # Invalid
            "status": "pending"
        }
        with pytest.raises(AssertionError, match="score must be between 0 and 1"):
            validate_dedupe_suggestion(suggestion)
    
    def test_dedupe_pair_uniqueness(self):
        """Test that dedupe pairs are unique (no duplicates)."""
        # This would be tested against actual DB results
        pairs = [
            {"rec_id_1": "001", "rec_id_2": "002", "score": 0.9},
            {"rec_id_1": "001", "rec_id_2": "003", "score": 0.8},
            {"rec_id_1": "002", "rec_id_2": "003", "score": 0.7},
        ]
        
        # Check uniqueness
        pair_keys = [(p["rec_id_1"], p["rec_id_2"]) for p in pairs]
        assert len(pair_keys) == len(set(pair_keys)), "Duplicate pairs found"
        
        # Check canonical ordering
        for pair in pairs:
            assert pair["rec_id_1"] < pair["rec_id_2"], "Non-canonical ordering"


class TestMergePlanContracts:
    """Test merge-plan API contracts."""
    
    def test_merge_plan_valid(self):
        """Test valid merge plan passes validation."""
        response = {
            "cluster_id": "cluster_123",
            "object_type": "Lead",
            "primary_id": "001123",  # Correct key name
            "fields": [
                {
                    "field": "Name",  # Correct key (not 'api')
                    "label": "Full Name",
                    "group": "Identity",
                    "hasConflict": False,
                    "locked": False,
                    "values": {  # Dict, not list
                        "001123": "John Doe",
                        "001456": "John Doe"
                    }
                },
                {
                    "field": "Email",
                    "label": "Email Address",
                    "group": "Contact",
                    "hasConflict": True,
                    "locked": False,
                    "values": {
                        "001123": "john@acme.com",
                        "001456": "jdoe@acme.com"
                    }
                }
            ],
            "candidates": [
                {"id": "001123", "score": 0.95, "tier": "CERTAIN"},
                {"id": "001456", "score": 0.85, "tier": "HIGH"}
            ],
            "conflict_count": 1  # Matches actual conflicts
        }
        validate_merge_plan_response(response)
    
    def test_merge_plan_wrong_field_key(self):
        """Test merge plan with wrong field key fails."""
        response = {
            "cluster_id": "cluster_123",
            "object_type": "Lead",
            "primary_id": "001123",
            "fields": [
                {
                    "api": "Name",  # Wrong key (should be 'field')
                    "label": "Full Name",
                    "group": "Identity",
                    "hasConflict": False,
                    "locked": False,
                    "values": {"001123": "John Doe"}
                }
            ],
            "candidates": []
        }
        with pytest.raises(AssertionError, match="Field must have 'field' key"):
            validate_merge_plan_response(response)
    
    def test_merge_plan_wrong_values_type(self):
        """Test merge plan with wrong values type fails."""
        response = {
            "cluster_id": "cluster_123",
            "object_type": "Lead",
            "primary_id": "001123",
            "fields": [
                {
                    "field": "Name",
                    "label": "Full Name",
                    "group": "Identity",
                    "hasConflict": False,
                    "locked": False,
                    "values": [  # Wrong type (should be dict)
                        {"record_id": "001123", "value": "John Doe"}
                    ]
                }
            ],
            "candidates": []
        }
        with pytest.raises(AssertionError, match="values must be a dict"):
            validate_merge_plan_response(response)
    
    def test_merge_plan_conflict_count_mismatch(self):
        """Test merge plan with incorrect conflict count fails."""
        response = {
            "cluster_id": "cluster_123",
            "object_type": "Lead",
            "primary_id": "001123",
            "fields": [
                {
                    "field": "Name",
                    "label": "Full Name",
                    "group": "Identity",
                    "hasConflict": False,  # No conflict
                    "locked": False,
                    "values": {"001123": "John Doe"}
                },
                {
                    "field": "Email",
                    "label": "Email",
                    "group": "Contact",
                    "hasConflict": True,  # Has conflict
                    "locked": False,
                    "values": {"001123": "john@acme.com", "001456": "j.doe@acme.com"}
                }
            ],
            "candidates": [],
            "conflict_count": 5  # Wrong! Should be 1
        }
        with pytest.raises(AssertionError, match="conflict_count mismatch"):
            validate_merge_plan_response(response)


class TestAPIEndpoints:
    """Test actual API endpoints for contract compliance."""
    
    @pytest.fixture
    def client(self):
        """Create test client."""
        # Import here to avoid circular dependencies
        from fmatch.saas.main import app
        return TestClient(app)
    
    @pytest.mark.skip(reason="Requires running server")
    def test_l2a_suggestions_endpoint(self, client):
        """Test L2A suggestions endpoint returns valid data."""
        response = client.get("/api/v2/l2a/suggestions?limit=5")
        assert response.status_code == 200
        
        data = response.json()
        assert "items" in data
        
        for suggestion in data["items"]:
            validate_l2a_suggestion(suggestion)
    
    @pytest.mark.skip(reason="Requires running server")
    def test_dedupe_suggestions_endpoint(self, client):
        """Test dedupe suggestions endpoint returns valid data."""
        response = client.get("/api/v2/dedupe/suggestions?object_type=Lead&limit=5")
        assert response.status_code == 200
        
        data = response.json()
        assert "items" in data
        
        for suggestion in data["items"]:
            validate_dedupe_suggestion(suggestion)
    
    @pytest.mark.skip(reason="Requires running server and data")
    def test_merge_plan_endpoint(self, client):
        """Test merge-plan endpoint returns valid data."""
        # Would need a valid cluster_id
        response = client.get("/api/v2/dedupe/merge-plan/test_cluster?object_type=Lead")
        if response.status_code == 200:
            data = response.json()
            validate_merge_plan_response(data)


class TestFailureScenarios:
    """Test that validation actually catches bad data."""
    
    def test_catches_score_above_one(self):
        """Ensure we catch scores > 1 that haven't been normalized."""
        suggestion = {
            "lead_id": "00Q123",
            "account_id": "001456", 
            "score": 95,  # Unnormalized 0-100 score
            "tier": "CERTAIN",
            "status": "pending"
        }
        with pytest.raises(AssertionError, match="score must be between 0 and 1"):
            validate_l2a_suggestion(suggestion)
    
    def test_catches_negative_score(self):
        """Ensure we catch negative scores."""
        suggestion = {
            "rec_id_1": "001123",
            "rec_id_2": "001456",
            "score": -0.5,
            "status": "pending"
        }
        with pytest.raises(AssertionError, match="score must be between 0 and 1"):
            validate_dedupe_suggestion(suggestion)
    
    def test_catches_missing_required_field(self):
        """Ensure we catch missing required fields."""
        suggestion = {
            "lead_id": "00Q123",
            # Missing account_id
            "score": 0.85,
            "tier": "LIKELY",
            "status": "pending"
        }
        with pytest.raises(AssertionError):
            validate_l2a_suggestion(suggestion)