
import pytest
import sys

from semantica.semantic_extract.ner_extractor import NERExtractor, Entity
from semantica.semantic_extract.relation_extractor import RelationExtractor, Relation
from semantica.semantic_extract.triplet_extractor import TripletExtractor

class TestRobustnessFallback:
    
    def test_ner_last_resort_fallback(self):
        """Test that NER extractor finds entities even in obscure text via last resort."""
        extractor = NERExtractor()
        
        # Text with single capitalized word that shouldn't match PERSON pattern (requires 2+ words)
        text = "Zylophone"
        
        entities = extractor.extract_entities(text)
        
        assert len(entities) > 0, "Should have extracted at least one entity via last resort"
        # Check if they are the capitalized words
        texts = [e.text for e in entities]
        assert "Zylophone" in texts
        
        # Verify metadata
        for e in entities:
            assert e.metadata is not None
            assert "extraction_method" in e.metadata
            # Should be last_resort_pattern
            assert e.metadata["extraction_method"] == "last_resort_pattern"
    
    def test_relation_last_resort_fallback(self):
        """Test that Relation extractor creates adjacency relations when no patterns match."""
        extractor = RelationExtractor()
        
        # Create entities far apart to avoid "co_occurrence" fallback which triggers < 100 chars
        padding = " " * 105
        text = f"Alpha{padding}Beta{padding}Gamma"
        
        # Alpha at start
        e1_start = 0
        e1_end = 5
        
        # Beta after padding
        e2_start = e1_end + 105
        e2_end = e2_start + 4
        
        # Gamma after padding
        e3_start = e2_end + 105
        e3_end = e3_start + 5
        
        e1 = Entity(text="Alpha", label="UNKNOWN", start_char=e1_start, end_char=e1_end)
        e2 = Entity(text="Beta", label="UNKNOWN", start_char=e2_start, end_char=e2_end)
        e3 = Entity(text="Gamma", label="UNKNOWN", start_char=e3_start, end_char=e3_end)
        
        entities = [e1, e2, e3]
        
        # This text has no "is a", "works for", etc. patterns.
        # And entities are too far for co-occurrence (< 100).
        # It should trigger the last resort adjacency fallback.
        relations = extractor.extract_relations(text, entities)
        
        assert len(relations) > 0, "Should have extracted relations via last resort"
        
        # Expect relations between adjacent entities: Alpha->Beta, Beta->Gamma
        pairs = [(r.subject.text, r.object.text) for r in relations]
        assert ("Alpha", "Beta") in pairs
        assert ("Beta", "Gamma") in pairs
        
        # Verify metadata
        for r in relations:
            assert r.metadata is not None
            assert "extraction_method" in r.metadata
            assert r.metadata.get("extraction_method") == "last_resort_adjacency"

    def test_triplet_fallback_conversion(self):
        """Test that Triplet extractor falls back to converting relations if extraction fails."""
        # Setup mocks or use real classes
        ner = NERExtractor() # We'll just pass entities directly
        rel_extractor = RelationExtractor()
        triplet_extractor = TripletExtractor()
        
        text = "Alpha is connected to Beta."
        e1 = Entity(text="Alpha", label="Thing", start_char=0, end_char=5)
        e2 = Entity(text="Beta", label="Thing", start_char=22, end_char=26)
        entities = [e1, e2]
        
        # Create a relation manually to ensure we have one to convert
        relation = Relation(
            subject=e1,
            predicate="connected_to",
            object=e2,
            confidence=0.9,
            context=text
        )
        
        # We want to test the fallback in extract_triplets.
        # Since we can't easily force the primary triplet method to return empty without mocking,
        # we can pass the relations explicitly and rely on the fact that standard triplet extraction
        # might not support "connected_to" if it relies on strict patterns, or we can use a method that fails.
        
        # However, the triplet extractor calls relation extractor internally if not provided.
        # Let's test the flow where we provide relations.
        
        triplets = triplet_extractor.extract_triplets(text, entities=entities, relations=[relation])
        
        assert len(triplets) > 0
        assert triplets[0].subject == "Alpha"
        assert triplets[0].object == "Beta"
        assert triplets[0].predicate == "connected_to"
        
    def test_batch_metadata_propagation(self):
        """Verify batch_index and document_id are propagated in batch mode with fallbacks."""
        ner = NERExtractor()
        
        docs = [
            {"content": "First doc", "id": "doc_1"},
            {"content": "Second doc", "id": "doc_2"}
        ]
        
        # These docs are simple, might trigger fallback or simple patterns
        results = ner.extract(docs)
        
        assert len(results) == 2
        
        # Check first doc results
        for e in results[0]:
            assert e.metadata["batch_index"] == 0
            assert e.metadata["document_id"] == "doc_1"
            
        # Check second doc results
        for e in results[1]:
            assert e.metadata["batch_index"] == 1
            assert e.metadata["document_id"] == "doc_2"

if __name__ == "__main__":
    # Manually run if executed as script
    t = TestRobustnessFallback()
    try:
        t.test_ner_last_resort_fallback()
        print("NER Fallback Test Passed")
        t.test_relation_last_resort_fallback()
        print("Relation Fallback Test Passed")
        t.test_triplet_fallback_conversion()
        print("Triplet Fallback Test Passed")
        t.test_batch_metadata_propagation()
        print("Batch Metadata Test Passed")
    except Exception as e:
        print(f"Test Failed: {e}")
        import traceback
        traceback.print_exc()
