"""
test_sample_selector.py - Comprehensive tests for SampleSelector
T101-T103: Unit tests + Regression tests for missing methods
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

import unittest
from unittest.mock import Mock, MagicMock, patch
import sample_selector as sample_selector_module
from sample_selector import SampleSelector, Sample, _extract_sample_family


class MockSample:
    """Mock Sample class for testing."""
    def __init__(self, name, sample_id, duration=1.0, rating=3.0, bpm=None, key=None,
                 category='drums', sample_type='kick', path='/test/', spectral_centroid=5000.0,
                 rms_energy=0.5, genres=None):
        self.name = name
        self.id = sample_id
        self.duration = duration
        self.rating = rating
        self.bpm = bpm
        self.key = key
        self.category = category
        self.sample_type = sample_type
        self.path = path + name if not path.endswith(name) else path
        self.file_path = self.path
        self.spectral_centroid = spectral_centroid
        self.rms_energy = rms_energy
        self.genres = genres or []
        self.subcategory = sample_type

    def to_dict(self):
        return {
            'name': self.name,
            'id': self.id,
            'duration': self.duration,
            'rating': self.rating,
            'bpm': self.bpm,
            'key': self.key,
            'category': self.category,
            'sample_type': self.sample_type,
            'path': self.path
        }


class TestSampleSelectorRepetitionPenalty(unittest.TestCase):
    """T-REGRESSION: Tests for _calculate_repetition_penalty method."""

    def setUp(self):
        self.selector = SampleSelector()

    def test_calculate_repetition_penalty_exists(self):
        """REGRESSION TEST: _calculate_repetition_penalty method must exist and be callable."""
        # This test will FAIL if the method is missing
        self.assertTrue(hasattr(self.selector, '_calculate_repetition_penalty'),
                       "CRITICAL: _calculate_repetition_penalty method is MISSING!")

        # Verify it's callable
        self.assertTrue(callable(getattr(self.selector, '_calculate_repetition_penalty')),
                       "CRITICAL: _calculate_repetition_penalty is not callable!")

    def test_repetition_penalty_returns_float(self):
        """Test that _calculate_repetition_penalty returns a float value."""
        sample = MockSample("kick_808.wav", "sample_1", duration=0.5)

        # This will crash if method doesn't exist
        result = self.selector._calculate_repetition_penalty(sample)

        self.assertIsNotNone(result, "_calculate_repetition_penalty returned None")
        self.assertIsInstance(result, float, "_calculate_repetition_penalty should return float")
        self.assertGreaterEqual(result, 0.0, "Penalty should be >= 0")
        self.assertLessEqual(result, 1.0, "Penalty should be <= 1")

    def test_repetition_penalty_decreases_for_recent_samples(self):
        """Test that recently used samples get lower penalty (are penalized more)."""
        sample1 = MockSample("kick_808.wav", "sample_1", duration=0.5)

        # First use - no penalty
        penalty1 = self.selector._calculate_repetition_penalty(sample1)
        self.assertEqual(penalty1, 1.0, "First sample should have no penalty")

        # Mark as used
        self.selector._recent_sample_ids.append(sample1.id)
        family = _extract_sample_family(sample1.name)
        self.selector._recent_families[family] = 1

        # Second check - should be penalized
        penalty2 = self.selector._calculate_repetition_penalty(sample1)
        self.assertLess(penalty2, 1.0, "Recently used sample should be penalized")

    def test_repetition_penalty_family_tracking(self):
        """Test that family repetition is tracked and penalized."""
        sample1 = MockSample("kick_808.wav", "sample_1")
        sample2 = MockSample("snare_808.wav", "sample_2")  # Same family

        # Use first sample
        family = _extract_sample_family(sample1.name)
        self.selector._recent_families[family] = 2  # Simulate 2 previous uses

        penalty = self.selector._calculate_repetition_penalty(sample2)
        self.assertLess(penalty, 1.0, "Sample from used family should be penalized")


class TestSampleSelectorScoringPipeline(unittest.TestCase):
    """Tests for the complete scoring pipeline."""

    def setUp(self):
        self.selector = SampleSelector()

    def test_calculate_sample_score_exists(self):
        """Test that _calculate_sample_score method exists."""
        self.assertTrue(hasattr(self.selector, '_calculate_sample_score'),
                       "_calculate_sample_score method is missing!")
        self.assertTrue(callable(getattr(self.selector, '_calculate_sample_score')),
                       "_calculate_sample_score is not callable!")

    def test_calculate_sample_score_returns_score(self):
        """Test that _calculate_sample_score returns a valid score."""
        sample = MockSample("kick_808.wav", "sample_1", rating=4.0, bpm=128, key="Am")

        # This will fail if _calculate_repetition_penalty is missing (called internally)
        score = self.selector._calculate_sample_score(
            sample,
            target_key="Am",
            target_bpm=128,
            target_role="kick",
            target_genre="techno",
            prefer_oneshot=True
        )

        self.assertIsNotNone(score, "Score should not be None")
        self.assertIsInstance(score, float, "Score should be a float")
        self.assertGreater(score, 0, "Score should be positive")

    def test_full_scoring_path_no_crash(self):
        """REGRESSION TEST: Full scoring path should not crash even with complex inputs."""
        samples = [
            MockSample("kick_808.wav", "s1", rating=4.0, bpm=128, key="Am"),
            MockSample("kick_909.wav", "s2", rating=3.5, bpm=130, key="Cm"),
            MockSample("kick_deep.wav", "s3", rating=5.0, bpm=128, key="Am"),
        ]

        # Set section context (used internally)
        self.selector.set_section_context("drop")

        # This tests the full path including _calculate_repetition_penalty
        for sample in samples:
            try:
                score = self.selector._calculate_sample_score(
                    sample,
                    target_key="Am",
                    target_bpm=128,
                    target_role="kick",
                    target_genre="techno"
                )
                self.assertIsNotNone(score, f"Score for {sample.name} is None")
                self.assertGreater(score, 0, f"Score for {sample.name} should be > 0")
            except AttributeError as e:
                self.fail(f"Full scoring path crashed (missing method?): {e}")
            except Exception as e:
                self.fail(f"Full scoring path crashed with error: {e}")


class TestSampleSelectorSectionContext(unittest.TestCase):
    """Tests for section-aware selection."""

    def setUp(self):
        self.selector = SampleSelector()

    def test_set_section_context_exists(self):
        """Test that set_section_context method exists."""
        self.assertTrue(hasattr(self.selector, 'set_section_context'),
                       "set_section_context method is missing!")

    def test_set_section_context_sets_value(self):
        """Test that set_section_context correctly sets the context."""
        self.selector.set_section_context("build")
        self.assertEqual(self.selector._section_context, "build")

        self.selector.set_section_context("drop")
        self.assertEqual(self.selector._section_context, "drop")

    def test_clear_section_context(self):
        """Test that clear_section_context clears the context."""
        self.selector.set_section_context("build")
        self.selector.clear_section_context()
        self.assertIsNone(self.selector._section_context)

    def test_get_section_role_bonus_exists(self):
        """Test that _get_section_role_bonus method exists."""
        self.assertTrue(hasattr(self.selector, '_get_section_role_bonus'),
                       "_get_section_role_bonus method is missing!")

    def test_section_role_bonus_returns_tuple(self):
        """Test that _get_section_role_bonus returns (bonus, reason) tuple."""
        self.selector.set_section_context("drop")

        result = self.selector._get_section_role_bonus("kick")
        self.assertIsInstance(result, tuple, "Should return tuple")
        self.assertEqual(len(result), 2, "Should return (bonus, reason)")

        bonus, reason = result
        self.assertIsInstance(bonus, float, "Bonus should be float")
        self.assertIsInstance(reason, str, "Reason should be string")

    def test_section_role_bonus_primary_roles(self):
        """Test that primary roles for section get higher bonus."""
        self.selector.set_section_context("drop")

        # Kick is primary for drop
        bonus_kick, _ = self.selector._get_section_role_bonus("kick")
        self.assertGreater(bonus_kick, 1.0, "Primary role should get bonus > 1.0")

        # Snare_roll is avoided in drop
        bonus_roll, _ = self.selector._get_section_role_bonus("snare_roll")
        self.assertLess(bonus_roll, 1.0, "Avoided role should get bonus < 1.0")


class TestSampleSelectorJointScoring(unittest.TestCase):
    """Tests for joint scoring functionality."""

    def setUp(self):
        self.selector = SampleSelector()

    def test_record_section_selection_exists(self):
        """Test that record_section_selection method exists."""
        self.assertTrue(hasattr(self.selector, 'record_section_selection'),
                       "record_section_selection method is missing!")

    def test_record_section_selection_stores_data(self):
        """Test that record_section_selection correctly stores selection data."""
        sample = MockSample("kick_808.wav", "sample_1", path="/test/kicks/")

        self.selector.record_section_selection("drop", "kick", sample)

        selections = self.selector.get_section_selections("drop")
        self.assertIn("kick", selections)
        self.assertEqual(selections["kick"]["name"], "kick_808.wav")

    def test_calculate_joint_score_exists(self):
        """Test that _calculate_joint_score method exists."""
        self.assertTrue(hasattr(self.selector, '_calculate_joint_score'),
                       "_calculate_joint_score method is missing!")

    def test_calculate_joint_score_returns_float(self):
        """Test that _calculate_joint_score returns a valid score multiplier."""
        sample = MockSample("clap_808.wav", "s2", path="/test/drums/")

        # Create a mock already-selected sample
        selected_kick = MockSample("kick_808.wav", "s1", path="/test/drums/")

        # Calculate joint score - should return multiplier
        result = self.selector._calculate_joint_score(
            sample,
            "clap",
            {"kick": selected_kick}
        )

        self.assertIsInstance(result, float, "Joint score should be float")
        self.assertGreater(result, 0, "Joint score should be positive")

    def test_joint_score_same_folder_bonus(self):
        """Test that samples from same folder get bonus in joint scoring."""
        sample1 = MockSample("kick_808.wav", "s1", path="/pack_a/kicks/")
        sample2 = MockSample("clap_808.wav", "s2", path="/pack_a/claps/")  # Same parent
        sample3 = MockSample("snare_other.wav", "s3", path="/pack_b/snares/")  # Different

        # Same folder/parent should get bonus
        score1 = self.selector._calculate_joint_score(sample2, "clap", {"kick": sample1})
        score2 = self.selector._calculate_joint_score(sample3, "snare", {"kick": sample1})

        self.assertGreater(score1, score2, "Same pack samples should get higher joint score")

    def test_joint_score_used_in_selection(self):
        """
        JOINT_SCORE must influence actual selection.
        
        This test WILL FAIL if:
        - JOINT_SCORE is calculated but ignored in final ranking
        - Selection uses base_score instead of JOINT_SCORE-adjusted score
        """
        selector = SampleSelector()
        
        # Setup context
        selector.set_section_context('drop')
        selector.record_section_selection('drop', 'kick', {'path': 'kick1.wav'})
        
        # Candidate that would be weak alone but strong in context
        # Mock sample in the same pack as selected kick
        candidate = MockSample('clap_sync.wav', 'c1', path='/pack_a/claps/clap_sync.wav', rating=3.5)
        
        # Calculate joint score - pass dict not list
        joint = selector._calculate_joint_score(
            candidate, 'clap', 
            {'kick': MockSample('kick1.wav', 'k1', path='/pack_a/kicks/kick1.wav')}
        )
        
        # ASSERT: Joint score should boost candidate
        self.assertGreater(joint, 1.0,
            f"CRITICAL: Joint score should boost compatible candidates (>1.0). "
            f"Got {joint}. If <= 1.0, JOINT_SCORE has no positive effect on selection!"
        )
        
        # Additional check: score should be meaningfully boosted
        self.assertGreaterEqual(joint, 1.1,
            f"CRITICAL: Joint score boost too weak: {joint}. "
            f"Should be at least 1.1x to influence selection."
        )
        
        # ASSERT: Joint score should boost candidate
        self.assertGreater(joint, 1.0,
            f"CRITICAL: Joint score should boost compatible candidates (>1.0). "
            f"Got {joint}. If <= 1.0, JOINT_SCORE has no positive effect on selection!"
        )
        
        # Additional check: score should be meaningfully boosted
        self.assertGreaterEqual(joint, 1.1,
            f"CRITICAL: Joint score boost too weak: {joint}. "
            f"Should be at least 1.1x to influence selection."
        )


class TestSampleSelectorExistingFeatures(unittest.TestCase):
    """Tests for existing palette and fatigue features."""

    def setUp(self):
        self.selector = SampleSelector()

    def test_palette_bonus_exact_match(self):
        """T026: Bonus 1.4x para folder ancla exacto."""
        self.selector.set_palette_data({'drums': '/samples/Kicks'})
        bonus = self.selector._calculate_palette_bonus('/samples/Kicks/kick_01.wav', '/samples/Kicks')
        self.assertEqual(bonus, 1.4)

    def test_palette_bonus_sibling_folder(self):
        """T026: Bonus 1.2x para folder hermano."""
        self.selector.set_palette_data({'drums': '/samples/Kicks'})
        bonus = self.selector._calculate_palette_bonus('/samples/Snares/snare_01.wav', '/samples/Kicks')
        self.assertEqual(bonus, 1.2)

    def test_palette_bonus_different_folder(self):
        """T026: Penalizacion 0.9x para folder completamente diferente."""
        self.selector.set_palette_data({'drums': '/Library/Kicks'})
        bonus = self.selector._calculate_palette_bonus('/OtherLibrary/Pads/pad.wav', '/Library/Kicks')
        self.assertEqual(bonus, 0.9)

    def test_role_to_bus_mapping(self):
        """Test mapeo de roles a buses."""
        self.assertEqual(self.selector._role_to_bus('kick'), 'drums')
        self.assertEqual(self.selector._role_to_bus('bass'), 'bass')
        self.assertEqual(self.selector._role_to_bus('synth'), 'music')

    def test_fatigue_calculation(self):
        """T022: CÃ¡lculo correcto de fatiga."""
        fatigue_data = {'/samples/kick_01.wav': {'kick': {'uses': 5}}}
        self.selector.set_fatigue_data(fatigue_data)
        factor = self.selector._get_persistent_fatigue('/samples/kick_01.wav', 'kick')
        self.assertEqual(factor, 0.50)


class TestSampleSelectorIntegration(unittest.TestCase):
    """Integration tests combining multiple features."""

    def setUp(self):
        self.selector = SampleSelector()

    def test_integration_section_and_scoring(self):
        """Test that section context integrates with scoring correctly."""
        # Set up context
        self.selector.set_section_context("build")

        # Record a selection
        kick_sample = MockSample("kick_main.wav", "s1", path="/pack_a/kicks/",
                                rating=4.0, bpm=128, key="Am", duration=0.5)
        self.selector.record_section_selection("build", "kick", kick_sample)

        # Now score another sample - should consider section context and joint scoring
        clap_sample = MockSample("clap_808.wav", "s2", path="/pack_a/claps/",
                                rating=4.0, bpm=128, key="Am", duration=0.3)

        try:
            score = self.selector._calculate_sample_score(
                clap_sample,
                target_key="Am",
                target_bpm=128,
                target_role="clap",
                target_genre="techno"
            )
            self.assertIsNotNone(score)
            self.assertGreater(score, 0)
        except AttributeError as e:
            self.fail(f"Integration test failed (missing method?): {e}")

    def test_comprehensive_scoring_with_all_features(self):
        """Test scoring with all features enabled (fatigue, palette, section, joint)."""
        # Set up all contexts
        self.selector.set_section_context("drop")
        self.selector.set_palette_data({'drums': '/pack_a/'})
        self.selector.set_fatigue_data({'/pack_a/kick_808.wav': {'kick': {'uses': 2}}})

        # Create sample
        sample = MockSample("kick_808.wav", "s1", path="/pack_a/kick_808.wav",
                           rating=5.0, bpm=128, key="Am", duration=0.5,
                           spectral_centroid=2000.0)

        try:
            score = self.selector._calculate_sample_score(
                sample,
                target_key="Am",
                target_bpm=128,
                target_role="kick",
                target_genre="techno",
                prefer_oneshot=True
            )
            self.assertIsNotNone(score)
            self.assertGreater(score, 0)
            # Score should be affected by all factors
        except AttributeError as e:
            self.fail(f"Comprehensive scoring failed (missing method?): {e}")


class TestCrossGenerationMemoryRegression(unittest.TestCase):
    def tearDown(self):
        sample_selector_module._cross_generation_family_memory.clear()
        sample_selector_module._cross_generation_path_memory.clear()

    def test_update_cross_generation_memory_accepts_new_keys_after_compaction(self):
        sample_selector_module._update_cross_generation_memory({"kick": 1}, ["/tmp/kick_a.wav"])
        self.assertEqual(sample_selector_module._cross_generation_family_memory.get("kick"), 1)
        self.assertEqual(sample_selector_module._cross_generation_path_memory.get("/tmp/kick_a.wav"), 1)

        sample_selector_module._update_cross_generation_memory({"snare": 2}, ["/tmp/snare_b.wav"])

        self.assertEqual(sample_selector_module._cross_generation_family_memory.get("snare"), 2)
        self.assertEqual(sample_selector_module._cross_generation_path_memory.get("/tmp/snare_b.wav"), 1)


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