import unittest
from unittest.mock import MagicMock, patch
import json
import sys
import os
import importlib

# Add parent directory to sys.path to import semantica
PARENT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
sys.path.insert(0, PARENT_DIR)

# Force reload modules
import semantica.utils.exceptions
importlib.reload(semantica.utils.exceptions)
import semantica.semantic_extract.methods
importlib.reload(semantica.semantic_extract.methods)
import semantica.semantic_extract.triplet_extractor
importlib.reload(semantica.semantic_extract.triplet_extractor)

from semantica.semantic_extract.methods import extract_entities_llm, extract_relations_llm, extract_triplets_llm
from semantica.semantic_extract.triplet_extractor import TripletExtractor, Triplet
from semantica.semantic_extract.ner_extractor import Entity
from semantica.utils.exceptions import ProcessingError

print(f"\nDEBUG: PARENT_DIR: {PARENT_DIR}")
print(f"DEBUG: sys.path[0]: {sys.path[0]}")
print(f"DEBUG: semantica.semantic_extract.methods file: {semantica.semantic_extract.methods.__file__}")
print(f"DEBUG: semantica.semantic_extract.triplet_extractor file: {semantica.semantic_extract.triplet_extractor.__file__}")

class TestLLMExtractionFixes(unittest.TestCase):

    @patch('semantica.semantic_extract.methods.create_provider')
    def test_raise_by_default(self, mock_create):
        """Test that methods raise ProcessingError by default on failure."""
        mock_llm = MagicMock()
        mock_llm.is_available.return_value = True
        mock_llm.generate_typed.side_effect = ProcessingError("LLM Error")
        mock_create.return_value = mock_llm

        try:
            extract_entities_llm("test text", provider="openai")
            self.fail("ProcessingError not raised")
        except ProcessingError as e:
            pass

    @patch('semantica.semantic_extract.methods.create_provider')
    def test_silent_fail_parameter(self, mock_create):
        """Test that silent_fail=True returns empty list instead of raising."""
        mock_llm = MagicMock()
        mock_llm.is_available.return_value = True
        mock_llm.generate_typed.side_effect = Exception("LLM Error")
        mock_create.return_value = mock_llm

        entities = extract_entities_llm("test text", provider="openai", silent_fail=True)
        self.assertEqual(entities, [])

    @patch('semantica.semantic_extract.methods.create_provider')
    def test_empty_text_validation(self, mock_create):
        """Test that empty text raises error or returns [] based on silent_fail."""
        with self.assertRaises(ProcessingError):
            extract_entities_llm("", provider="openai")
        
        self.assertEqual(extract_entities_llm("", provider="openai", silent_fail=True), [])

    def test_triplet_extractor_shadowing_fix(self):
        """Test that TripletExtractor.validate_triplets is not shadowed by an attribute."""
        extractor = TripletExtractor(validate=True)
        
        # DEBUG
        import inspect
        source = inspect.getsource(extractor.__init__)
        print(f"\nTripletExtractor.__init__ source snippet:\n{source[:200]}")
        
        self.assertTrue(callable(extractor.validate_triplets), "validate_triplets should be a method, not a bool")
        
        # Test delegation
        triplets = [Triplet(subject="s", predicate="p", object="o", confidence=0.1)]
        validated = extractor.validate_triplets(triplets, min_confidence=0.5)
        self.assertEqual(len(validated), 0)

    @patch('semantica.semantic_extract.methods.create_provider')
    def test_chunking_detection(self, mock_create):
        """Test that long text triggers chunking."""
        mock_llm = MagicMock()
        mock_llm.is_available.return_value = True
        mock_llm.generate_typed.return_value = MagicMock(entities=[]) # Mock response
        mock_create.return_value = mock_llm

        long_text = "This is a long text that should be chunked into multiple pieces."
        with patch('semantica.semantic_extract.methods._extract_entities_chunked') as mock_chunked:
            mock_chunked.return_value = []
            extract_entities_llm(long_text, max_text_length=10)
            mock_chunked.assert_called_once()

    @patch('semantica.semantic_extract.methods.create_provider')
    def test_relation_extraction_validation(self, mock_create):
        """Test that relation extraction validates entities list."""
        with self.assertRaises(ProcessingError):
            extract_relations_llm("text", entities=[], provider="openai")

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