
import unittest
import sys
import os
from unittest.mock import MagicMock, patch
from dataclasses import asdict

# Add project root to path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

from semantica.semantic_extract.ner_extractor import NERExtractor, Entity
from semantica.semantic_extract.named_entity_recognizer import NamedEntityRecognizer
from semantica.semantic_extract.methods import get_entity_method

class TestNERConfigurations(unittest.TestCase):
    """
    Test suite to verify NER with different configurations:
    - LLM
    - ML (spaCy)
    - Regex
    - Pattern
    - Fallbacks and Ensemble
    """

    def setUp(self):
        self.text = "Apple Inc. was founded by Steve Jobs."

    @patch('semantica.semantic_extract.methods.create_provider')
    def test_ner_llm_config(self, mock_create_provider):
        """Test NER with LLM configuration"""
        print("\nTesting NER with LLM configuration...")
        
        # Mock LLM provider
        mock_provider = MagicMock()
        mock_provider.is_available.return_value = True
        mock_provider.generate_structured.return_value = [
            {"text": "Apple Inc.", "label": "ORG", "start": 0, "end": 10, "confidence": 0.95},
            {"text": "Steve Jobs", "label": "PERSON", "start": 26, "end": 36, "confidence": 0.98}
        ]
        mock_create_provider.return_value = mock_provider
        
        # Initialize extractor with LLM method
        extractor = NERExtractor(
            method="llm", 
            provider="openai", 
            model="gpt-4",
            temperature=0.1
        )
        
        entities = extractor.extract_entities(self.text)
        
        # Verify provider creation args
        mock_create_provider.assert_called_with("openai", model="gpt-4", temperature=0.1)
        
        # Verify extraction
        self.assertEqual(len(entities), 2)
        self.assertEqual(entities[0].text, "Apple Inc.")
        self.assertEqual(entities[0].label, "ORG")
        self.assertEqual(entities[0].metadata["extraction_method"], "llm")
        self.assertEqual(entities[0].metadata["model"], "gpt-4")

    @patch('semantica.semantic_extract.methods.spacy')
    def test_ner_ml_config_spacy_available(self, mock_spacy):
        """Test NER with ML (spaCy) configuration when spaCy is available"""
        print("\nTesting NER with ML (spaCy) configuration...")
        
        # Mock spaCy nlp model
        mock_nlp = MagicMock()
        mock_doc = MagicMock()
        
        # Mock entities
        ent1 = MagicMock()
        ent1.text = "Apple Inc."
        ent1.label_ = "ORG"
        ent1.start_char = 0
        ent1.end_char = 10
        ent1.confidence = 1.0 # Optional attribute
        
        ent2 = MagicMock()
        ent2.text = "Steve Jobs"
        ent2.label_ = "PERSON"
        ent2.start_char = 26
        ent2.end_char = 36
        ent2.confidence = 0.99
        
        mock_doc.ents = [ent1, ent2]
        mock_nlp.return_value = mock_doc
        mock_spacy.load.return_value = mock_nlp
        
        # Patch SPACY_AVAILABLE in methods module
        with patch('semantica.semantic_extract.methods.SPACY_AVAILABLE', True):
            extractor = NERExtractor(method="ml", model="en_core_web_trf")
            entities = extractor.extract_entities(self.text)
            
            # Verify spacy load called with correct model
            mock_spacy.load.assert_called_with("en_core_web_trf")
            
            self.assertEqual(len(entities), 2)
            self.assertEqual(entities[0].text, "Apple Inc.")
            self.assertEqual(entities[0].label, "ORG")
            self.assertEqual(entities[0].metadata["extraction_method"], "ml")
            self.assertEqual(entities[0].metadata["model"], "en_core_web_trf")

    def test_ner_regex_config(self):
        """Test NER with Regex configuration"""
        print("\nTesting NER with Regex configuration...")
        
        custom_patterns = {
            "COMPANY": r"Apple Inc\.",
            "FOUNDER": r"Steve Jobs"
        }
        
        extractor = NERExtractor(method="regex", patterns=custom_patterns)
        entities = extractor.extract_entities(self.text)
        
        self.assertEqual(len(entities), 2)
        
        # Check if labels match custom keys
        labels = sorted([e.label for e in entities])
        self.assertEqual(labels, ["COMPANY", "FOUNDER"])
        
        # Check metadata
        self.assertEqual(entities[0].metadata["extraction_method"], "regex")

    def test_ner_pattern_config(self):
        """Test NER with default Pattern configuration"""
        print("\nTesting NER with Pattern configuration...")
        
        # Default patterns in methods.py match "Apple Inc" (ORG) and "Steve Jobs" (PERSON)
        # Note: The pattern for ORG in methods.py expects "Inc|Corp..."
        
        extractor = NERExtractor(method="pattern")
        entities = extractor.extract_entities(self.text)
        
        self.assertTrue(len(entities) >= 2)
        texts = [e.text for e in entities]
        self.assertIn("Apple Inc", texts) # Regex pattern does not capture the trailing dot
        # Actually methods.py regex: r"\b([A-Z][a-zA-Z]+(?:\s+[A-Z][a-zA-Z]+)*\s+(?:Inc|Corp|LLC|Ltd|Company))\b"
        # "Apple Inc." -> "Apple Inc" (dot is outside \b if not matched?)
        # Let's check the result strictly
        
    @patch('semantica.semantic_extract.methods.create_provider')
    @patch('semantica.semantic_extract.methods.spacy')
    def test_ner_ensemble_config(self, mock_spacy, mock_create_provider):
        """Test NER with Ensemble (Multiple Methods)"""
        print("\nTesting NER with Ensemble configuration...")
        
        # Setup mocks
        # LLM returns 1 entity
        mock_provider = MagicMock()
        mock_provider.is_available.return_value = True
        mock_provider.generate_structured.return_value = [
             {"text": "Apple Inc.", "label": "ORG", "start": 0, "end": 10, "confidence": 0.95}
        ]
        mock_create_provider.return_value = mock_provider
        
        # ML returns 2 entities
        mock_nlp = MagicMock()
        mock_doc = MagicMock()
        ent1 = MagicMock()
        ent1.text = "Apple Inc."
        ent1.label_ = "ORG"
        ent1.start_char = 0
        ent1.end_char = 10
        ent1.confidence = 0.95
        ent2 = MagicMock()
        ent2.text = "Steve Jobs"
        ent2.label_ = "PERSON"
        ent2.start_char = 26
        ent2.end_char = 36
        ent2.confidence = 0.99
        mock_doc.ents = [ent1, ent2]
        mock_nlp.return_value = mock_doc
        mock_spacy.load.return_value = mock_nlp
        
        with patch('semantica.semantic_extract.methods.SPACY_AVAILABLE', True):
            # Init extractor with list of methods
            extractor = NERExtractor(method=["llm", "ml"], ensemble_voting=True)
            entities = extractor.extract_entities(self.text)
            
            # Since ensemble_voting=True (implied merge), we expect unique entities
            # Apple Inc (from both) + Steve Jobs (from ML)
            
            texts = [e.text for e in entities]
            self.assertIn("Apple Inc.", texts)
            self.assertIn("Steve Jobs", texts)

    @patch('semantica.semantic_extract.methods.HuggingFaceModelLoader')
    def test_ner_huggingface_config(self, mock_loader_cls):
        """Test NER with HuggingFace configuration"""
        print("\nTesting NER with HuggingFace configuration...")
        
        mock_loader = MagicMock()
        mock_loader_cls.return_value = mock_loader
        
        # Mock extract_entities return
        # HuggingFace loader typically returns list of dicts or objects
        mock_loader.extract_entities.return_value = [
            {"word": "Apple Inc.", "entity_group": "ORG", "score": 0.99, "start": 0, "end": 10}
        ]
        
        extractor = NERExtractor(
            method="huggingface", 
            huggingface_model="dslim/bert-base-NER",
            device="cpu"
        )
        entities = extractor.extract_entities(self.text)
        
        mock_loader.load_ner_model.assert_called_with("dslim/bert-base-NER")
        self.assertEqual(len(entities), 1)
        self.assertEqual(entities[0].text, "Apple Inc.")
        self.assertEqual(entities[0].label, "ORG")

if __name__ == '__main__':
    unittest.main()
