"""
test_arrangement_intelligence.py - Tests para ArrangementIntelligence.

Valida T086-T094: estructuras reggaeton, mute throws, curvas de energia.
"""

import json
import os
import sys
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch

SCRIPT_DIR = Path(__file__).resolve().parent
SERVER_DIR = SCRIPT_DIR.parent
if str(SERVER_DIR) not in sys.path:
    sys.path.insert(0, str(SERVER_DIR))

from arrangement_intelligence import (
    REGGAETON_STRUCTURE_95BPM,
    MUTE_THROW_WINDOWS,
    ROLE_TO_TRACK_INDEX_MAP,
    HARMONIC_TRACK_INDEX,
    TOP_LOOP_TRACK_INDEX,
    PERC_ALT_TRACK_INDEX,
    SectionInfo,
    EnergyCurveResult,
    ArrangementIntelligence,
)


class TestReggaetonStructure(unittest.TestCase):
    """Tests para la estructura reggaeton 95 BPM."""

    def test_reggaeton_structure_exists(self):
        """La estructura reggaeton tiene todas las secciones."""
        self.assertIn('intro', REGGAETON_STRUCTURE_95BPM)
        self.assertIn('build_a', REGGAETON_STRUCTURE_95BPM)
        self.assertIn('drop_a', REGGAETON_STRUCTURE_95BPM)
        self.assertIn('break', REGGAETON_STRUCTURE_95BPM)
        self.assertIn('build_b', REGGAETON_STRUCTURE_95BPM)
        self.assertIn('drop_b', REGGAETON_STRUCTURE_95BPM)
        self.assertIn('outro', REGGAETON_STRUCTURE_95BPM)

    def test_reggaeton_structure_timing(self):
        """Las secciones tienen timing correcto."""
        intro = REGGAETON_STRUCTURE_95BPM['intro']
        self.assertEqual(intro['start'], 0)
        self.assertEqual(intro['length'], 32)
        
        drop_a = REGGAETON_STRUCTURE_95BPM['drop_a']
        self.assertEqual(drop_a['start'], 64)
        self.assertEqual(drop_a['length'], 64)
        
        outro = REGGAETON_STRUCTURE_95BPM['outro']
        self.assertEqual(outro['start'], 256)
        self.assertEqual(outro['length'], 32)

    def test_reggaeton_energy_curve(self):
        """La curva de energia tiene sentido logico."""
        intro_energy = REGGAETON_STRUCTURE_95BPM['intro']['energy']
        build_a_energy = REGGAETON_STRUCTURE_95BPM['build_a']['energy']
        drop_a_energy = REGGAETON_STRUCTURE_95BPM['drop_a']['energy']
        break_energy = REGGAETON_STRUCTURE_95BPM['break']['energy']
        
        self.assertLess(intro_energy, build_a_energy)
        self.assertLess(build_a_energy, drop_a_energy)
        self.assertLess(break_energy, build_a_energy)

    def test_reggaeton_layers(self):
        """Cada seccion tiene layers definidos."""
        for section_name, section_data in REGGAETON_STRUCTURE_95BPM.items():
            with self.subTest(section=section_name):
                self.assertIn('layers', section_data)
                self.assertIsInstance(section_data['layers'], list)
                self.assertGreater(len(section_data['layers']), 0)

    def test_reggaeton_total_length(self):
        """El total del arrangement es 288 beats (32 + 32 + 64 + 32 + 32 + 64 + 32)."""
        total = 0
        for section_name, section_data in REGGAETON_STRUCTURE_95BPM.items():
            total += section_data['length']
        self.assertEqual(total, 288)


class TestMuteThrowWindows(unittest.TestCase):
    """Tests para las ventanas de mute throws."""

    def test_mute_throw_windows_exist(self):
        """Existen mute throws configurados."""
        self.assertGreater(len(MUTE_THROW_WINDOWS), 0)

    def test_mute_throw_before_drop_a(self):
        """Mute throw antes de drop_a esta configurado."""
        drop_a_throw = None
        for window in MUTE_THROW_WINDOWS:
            if window['before_section'] == 'drop_a':
                drop_a_throw = window
                break
        
        self.assertIsNotNone(drop_a_throw)
        self.assertEqual(drop_a_throw['start_beat'], 61)
        self.assertEqual(drop_a_throw['end_beat'], 64)
        self.assertIn('kick', drop_a_throw['layers_to_mute'])

    def test_mute_throw_before_drop_b(self):
        """Mute throw antes de drop_b esta configurado."""
        drop_b_throw = None
        for window in MUTE_THROW_WINDOWS:
            if window['before_section'] == 'drop_b':
                drop_b_throw = window
                break
        
        self.assertIsNotNone(drop_b_throw)
        self.assertEqual(drop_b_throw['start_beat'], 189)
        self.assertEqual(drop_b_throw['end_beat'], 192)

    def test_mute_throw_layers_valid(self):
        """Los layers a mutear son roles validos."""
        valid_roles = set(ROLE_TO_TRACK_INDEX_MAP.keys())
        for window in MUTE_THROW_WINDOWS:
            for layer in window['layers_to_mute']:
                with self.subTest(layer=layer):
                    self.assertIn(layer, valid_roles)


class TestRoleToTrackIndexMap(unittest.TestCase):
    """Tests para el mapeo de roles a indices de track."""

    def test_kick_track_index(self):
        """Kick siempre en track 0."""
        self.assertEqual(ROLE_TO_TRACK_INDEX_MAP['kick'], 0)

    def test_all_roles_have_indices(self):
        """Todos los roles tienen indices de track asignados."""
        expected_roles = ['kick', 'clap', 'hat', 'bass', 'perc_main', 'perc_alt', 
                         'synth', 'top_loop', 'atmos', 'hat_open', 'snare']
        for role in expected_roles:
            with self.subTest(role=role):
                self.assertIn(role, ROLE_TO_TRACK_INDEX_MAP)

    def test_harmonic_track_index(self):
        """Indice de track armonico esta definido."""
        self.assertIsInstance(HARMONIC_TRACK_INDEX, int)
        self.assertGreaterEqual(HARMONIC_TRACK_INDEX, 0)

    def test_special_track_indices(self):
        """Indices especiales estan definidos."""
        self.assertIsNotNone(TOP_LOOP_TRACK_INDEX)
        self.assertIsNotNone(PERC_ALT_TRACK_INDEX)


class TestSectionInfo(unittest.TestCase):
    """Tests para la dataclass SectionInfo."""

    def test_section_info_creation(self):
        """SectionInfo se crea correctamente."""
        section = SectionInfo(
            name='test_section',
            start=0.0,
            end=32.0,
            energy=0.5,
            layers=['kick', 'bass']
        )
        
        self.assertEqual(section.name, 'test_section')
        self.assertEqual(section.start, 0.0)
        self.assertEqual(section.end, 32.0)
        self.assertEqual(section.energy, 0.5)
        self.assertEqual(section.layers, ['kick', 'bass'])

    def test_section_info_length_property(self):
        """La propiedad length se calcula correctamente."""
        section = SectionInfo(
            name='test',
            start=64.0,
            end=128.0,
            energy=1.0,
            layers=[]
        )
        
        self.assertEqual(section.length, 64.0)

    def test_section_info_to_dict(self):
        """SectionInfo se serializa a dict correctamente."""
        section = SectionInfo(
            name='drop',
            start=64.0,
            end=128.0,
            energy=1.0,
            layers=['kick', 'bass', 'synth']
        )
        
        d = section.to_dict()
        
        self.assertIsInstance(d, dict)
        self.assertEqual(d['name'], 'drop')
        self.assertEqual(d['start'], 64.0)
        self.assertEqual(d['end'], 128.0)
        self.assertEqual(d['length'], 64.0)
        self.assertEqual(d['energy'], 1.0)
        self.assertEqual(d['layers'], ['kick', 'bass', 'synth'])


class TestEnergyCurveResult(unittest.TestCase):
    """Tests para EnergyCurveResult."""

    def test_energy_curve_result_creation(self):
        """EnergyCurveResult se crea correctamente."""
        result = EnergyCurveResult(
            score=0.85,
            sections_analyzed=7,
            sections_with_correct_energy=6,
            deviations=[{'section': 'break', 'expected': 0.2, 'actual': 0.4}],
            recommendations=['Increase energy in break section']
        )
        
        self.assertEqual(result.score, 0.85)
        self.assertEqual(result.sections_analyzed, 7)
        self.assertEqual(result.sections_with_correct_energy, 6)

    def test_energy_curve_result_to_dict(self):
        """EnergyCurveResult se serializa correctamente."""
        result = EnergyCurveResult(
            score=0.85,
            sections_analyzed=7,
            sections_with_correct_energy=6,
            deviations=[],
            recommendations=[]
        )
        
        d = result.to_dict()
        
        self.assertEqual(d['score'], 0.85)
        self.assertEqual(d['sections_analyzed'], 7)


class TestArrangementIntelligence(unittest.TestCase):
    """Tests para la clase ArrangementIntelligence."""

    def test_arrangement_intelligence_init(self):
        """ArrangementIntelligence inicializa correctamente."""
        ai = ArrangementIntelligence()
        self.assertIsNotNone(ai)

    def test_get_section_at_start(self):
        """get_section_at_beat retorna seccion correcta al inicio."""
        ai = ArrangementIntelligence()
        
        section = ai.get_section_at_beat(0)
        self.assertIsNotNone(section)
        self.assertEqual(section.name, 'intro')

    def test_get_section_at_drop(self):
        """get_section_at_beat retorna drop correcto."""
        ai = ArrangementIntelligence()
        
        section = ai.get_section_at_beat(80)
        self.assertIsNotNone(section)
        self.assertEqual(section.name, 'drop_a')

    def test_get_section_at_outro(self):
        """get_section_at_beat retorna outro correctamente."""
        ai = ArrangementIntelligence()
        
        section = ai.get_section_at_beat(270)
        self.assertIsNotNone(section)
        self.assertEqual(section.name, 'outro')

    def test_get_sections_by_energy(self):
        """get_sections_by_energy retorna secciones en rango de energia."""
        ai = ArrangementIntelligence()
        
        low_energy_sections = ai.get_sections_by_energy(0.0, 0.4)
        self.assertIsInstance(low_energy_sections, list)
        
        high_energy_sections = ai.get_sections_by_energy(0.8, 1.0)
        self.assertIsInstance(high_energy_sections, list)
        self.assertGreater(len(high_energy_sections), 0)
        
        for section in high_energy_sections:
            self.assertGreaterEqual(section.energy, 0.8)

    def test_get_mute_throw_positions(self):
        """get_mute_throw_positions retorna posiciones de mute throws."""
        ai = ArrangementIntelligence()
        
        positions = ai.get_mute_throw_positions()
        self.assertIsInstance(positions, list)

    def test_check_energy_curve_valid(self):
        """check_energy_curve valida curva de energia."""
        ai = ArrangementIntelligence()
        
        mock_tracks = {
            'Drums': [{'start': 0, 'length': 64}],
            'Bass': [{'start': 0, 'length': 128}],
        }
        
        result = ai.check_energy_curve(mock_tracks)
        
        self.assertIsInstance(result, EnergyCurveResult)
        self.assertGreaterEqual(result.score, 0.0)
        self.assertLessEqual(result.score, 1.0)


class TestMuteThrowLogic(unittest.TestCase):
    """Tests para logica de mute throws."""

    def test_get_mute_throw_positions(self):
        """get_mute_throw_positions retorna lista de mute throws."""
        ai = ArrangementIntelligence()
        
        positions = ai.get_mute_throw_positions()
        self.assertIsInstance(positions, list)

    def test_mute_throw_before_drop(self):
        """Mute throws existen antes de los drops."""
        ai = ArrangementIntelligence()
        
        positions = ai.get_mute_throw_positions()
        drop_positions = [p for p in positions if 'drop' in p.get('before_section', '')]
        
        self.assertGreater(len(drop_positions), 0)


class TestArrangementValidation(unittest.TestCase):
    """Tests de validacion de arrangement."""

    def test_validate_section_order(self):
        """Las secciones estan en orden correcto."""
        sections = list(REGGAETON_STRUCTURE_95BPM.keys())
        expected_order = ['intro', 'build_a', 'drop_a', 'break', 'build_b', 'drop_b', 'outro']
        self.assertEqual(sections, expected_order)

    def test_validate_no_overlapping_sections(self):
        """Las secciones no se superponen."""
        previous_end = 0
        for section_name, section_data in REGGAETON_STRUCTURE_95BPM.items():
            with self.subTest(section=section_name):
                self.assertEqual(section_data['start'], previous_end)
                previous_end = section_data['start'] + section_data['length']


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