"""Unit tests for music theory functions."""
import unittest

from reaper_mcp.midi.music_theory import (
    get_scale_notes,
    transpose_notes,
    find_relative_key,
    suggest_chord_progression,
    quantize_notes_to_scale,
)


class TestGetScaleNotes(unittest.TestCase):
    """Tests for get_scale_notes function."""
    
    def test_c_major_scale(self):
        """Test C major scale generation."""
        result = get_scale_notes.fn("C", "major", 4)
        
        self.assertNotIn("error", result)
        self.assertEqual(result["scale"], "C major")
        self.assertEqual(result["midi_notes"], [60, 62, 64, 65, 67, 69, 71])
        self.assertEqual(result["note_names"], ["C4", "D4", "E4", "F4", "G4", "A4", "B4"])
    
    def test_a_minor_scale(self):
        """Test A minor scale generation."""
        result = get_scale_notes.fn("A", "minor", 4)
        
        self.assertNotIn("error", result)
        self.assertEqual(result["scale"], "A minor")
        self.assertEqual(len(result["midi_notes"]), 7)
        self.assertEqual(result["midi_notes"][0], 69)  # A4
    
    def test_f_sharp_major(self):
        """Test F# major scale."""
        result = get_scale_notes.fn("F#", "major", 4)
        
        self.assertNotIn("error", result)
        self.assertEqual(result["scale"], "F# major")
        self.assertEqual(result["midi_notes"][0], 66)  # F#4
    
    def test_pentatonic_scale(self):
        """Test pentatonic scale generation."""
        result = get_scale_notes.fn("C", "pentatonic_major", 4)
        
        self.assertNotIn("error", result)
        self.assertEqual(len(result["midi_notes"]), 5)
        self.assertEqual(result["midi_notes"], [60, 62, 64, 67, 69])
    
    def test_blues_scale(self):
        """Test blues scale generation."""
        result = get_scale_notes.fn("C", "blues", 4)
        
        self.assertNotIn("error", result)
        self.assertEqual(len(result["midi_notes"]), 6)
    
    def test_invalid_key(self):
        """Test with invalid key."""
        result = get_scale_notes.fn("X", "major", 4)
        
        self.assertIn("error", result)
    
    def test_invalid_scale_type(self):
        """Test with invalid scale type."""
        result = get_scale_notes.fn("C", "invalid_scale", 4)
        
        self.assertIn("error", result)
    
    def test_different_octaves(self):
        """Test scales in different octaves."""
        result3 = get_scale_notes.fn("C", "major", 3)
        result5 = get_scale_notes.fn("C", "major", 5)
        
        self.assertNotIn("error", result3)
        self.assertNotIn("error", result5)
        # Octave 5 should be 12 semitones higher than octave 3
        self.assertEqual(result5["midi_notes"][0] - result3["midi_notes"][0], 24)


class TestTransposeNotes(unittest.TestCase):
    """Tests for transpose_notes function."""
    
    def test_transpose_up(self):
        """Test transposing notes up."""
        notes = [
            {"pitch": 60, "velocity": 100},
            {"pitch": 64, "velocity": 100},
            {"pitch": 67, "velocity": 100},
        ]
        result = transpose_notes.fn(notes, 2)
        
        self.assertNotIn("error", result)
        self.assertEqual(len(result["notes"]), 3)
        self.assertEqual(result["notes"][0]["pitch"], 62)
        self.assertEqual(result["notes"][1]["pitch"], 66)
        self.assertEqual(result["notes"][2]["pitch"], 69)
    
    def test_transpose_down(self):
        """Test transposing notes down."""
        notes = [{"pitch": 60}]
        result = transpose_notes.fn(notes, -3)
        
        self.assertNotIn("error", result)
        self.assertEqual(result["notes"][0]["pitch"], 57)
    
    def test_transpose_zero(self):
        """Test transposing by zero semitones."""
        notes = [{"pitch": 60}]
        result = transpose_notes.fn(notes, 0)
        
        self.assertNotIn("error", result)
        self.assertEqual(result["notes"][0]["pitch"], 60)
    
    def test_transpose_preserves_other_properties(self):
        """Test that transposition preserves other note properties."""
        notes = [{"pitch": 60, "velocity": 80, "duration": 1.0, "custom": "data"}]
        result = transpose_notes.fn(notes, 5)
        
        self.assertNotIn("error", result)
        transposed = result["notes"][0]
        self.assertEqual(transposed["pitch"], 65)
        self.assertEqual(transposed["velocity"], 80)
        self.assertEqual(transposed["duration"], 1.0)
        self.assertEqual(transposed["custom"], "data")
    
    def test_transpose_clamps_to_midi_range(self):
        """Test that transposition clamps to valid MIDI range (0-127)."""
        notes = [{"pitch": 125}]
        result = transpose_notes.fn(notes, 10)
        
        self.assertNotIn("error", result)
        # Should clamp to 127
        self.assertEqual(result["notes"][0]["pitch"], 127)
        
        notes2 = [{"pitch": 2}]
        result2 = transpose_notes.fn(notes2, -10)
        
        self.assertNotIn("error", result2)
        # Should clamp to 0
        self.assertEqual(result2["notes"][0]["pitch"], 0)
    
    def test_transpose_empty_list(self):
        """Test transposing empty note list (should return error)."""
        result = transpose_notes.fn([], 5)
        
        # Implementation returns error for empty list
        self.assertIn("error", result)


class TestFindRelativeKey(unittest.TestCase):
    """Tests for find_relative_key function."""
    
    def test_major_to_relative_minor(self):
        """Test finding relative minor from major key."""
        result = find_relative_key.fn("C major")
        
        self.assertNotIn("error", result)
        self.assertEqual(result["relative_key"], "A minor")
    
    def test_minor_to_relative_major(self):
        """Test finding relative major from minor key."""
        result = find_relative_key.fn("A minor")
        
        self.assertNotIn("error", result)
        self.assertEqual(result["relative_key"], "C major")
    
    def test_sharp_key(self):
        """Test with sharp key."""
        result = find_relative_key.fn("G major")
        
        self.assertNotIn("error", result)
        self.assertEqual(result["relative_key"], "E minor")
    
    def test_flat_key(self):
        """Test with sharp key A#."""
        result = find_relative_key.fn("A# major")
        
        self.assertNotIn("error", result)
        # A# major -> relative minor
        self.assertIn("minor", result["relative_key"])
    
    def test_mode_case_insensitive(self):
        """Test that mode (major/minor) is case-insensitive."""
        result1 = find_relative_key.fn("C Major")
        result2 = find_relative_key.fn("C major")
        result3 = find_relative_key.fn("C MAJOR")
        
        self.assertNotIn("error", result1)
        self.assertNotIn("error", result2)
        self.assertNotIn("error", result3)
        # All should return same result
        self.assertEqual(result1["relative_key"], result2["relative_key"])
        self.assertEqual(result2["relative_key"], result3["relative_key"])
    
    def test_invalid_key_format(self):
        """Test with invalid key format."""
        result = find_relative_key.fn("invalid")
        
        self.assertIn("error", result)


class TestSuggestChordProgression(unittest.TestCase):
    """Tests for suggest_chord_progression function."""
    
    def test_happy_progression(self):
        """Test suggesting happy chord progression."""
        result = suggest_chord_progression.fn("C", "happy", 4)
        
        self.assertNotIn("error", result)
        self.assertEqual(result["key"], "C")
        self.assertEqual(result["mood"], "happy")
        self.assertEqual(len(result["progression_roman"]), 4)
        self.assertIn("progression_chords", result)
    
    def test_sad_progression(self):
        """Test suggesting sad chord progression."""
        result = suggest_chord_progression.fn("A", "sad", 4)
        
        self.assertNotIn("error", result)
        self.assertEqual(result["mood"], "sad")
    
    def test_different_lengths(self):
        """Test progressions of different lengths."""
        result8 = suggest_chord_progression.fn("C", "happy", 8)
        result2 = suggest_chord_progression.fn("C", "happy", 2)
        
        self.assertNotIn("error", result8)
        self.assertNotIn("error", result2)
        self.assertEqual(len(result8["progression_roman"]), 8)
        self.assertEqual(len(result2["progression_roman"]), 2)
    
    def test_all_moods(self):
        """Test all available moods."""
        moods = ["happy", "sad", "dark", "uplifting", "energetic"]
        
        for mood in moods:
            result = suggest_chord_progression.fn("C", mood, 4)
            self.assertNotIn("error", result, f"Failed for mood: {mood}")
            self.assertEqual(result["mood"], mood)
    
    def test_invalid_mood(self):
        """Test with invalid mood (should return error)."""
        result = suggest_chord_progression.fn("C", "invalid_mood", 4)
        
        # Implementation returns error for invalid mood
        self.assertIn("error", result)


class TestQuantizeNotesToScale(unittest.TestCase):
    """Tests for quantize_notes_to_scale function."""
    
    def test_quantize_to_c_major(self):
        """Test quantizing notes to C major scale."""
        notes = [
            {"pitch": 60},  # C - in scale
            {"pitch": 61},  # C# - not in scale, should move to 60 or 62
            {"pitch": 64},  # E - in scale
        ]
        result = quantize_notes_to_scale.fn(notes, "C", "major")
        
        self.assertNotIn("error", result)
        quantized = result["notes"]
        self.assertEqual(len(quantized), 3)
        # First note should stay at 60
        self.assertEqual(quantized[0]["pitch"], 60)
        # Third note should stay at 64
        self.assertEqual(quantized[2]["pitch"], 64)
        # Second note should be quantized to nearest scale note
        self.assertIn(quantized[1]["pitch"], [60, 62])
    
    def test_quantize_preserves_properties(self):
        """Test that quantization preserves other note properties."""
        notes = [{"pitch": 61, "velocity": 80, "duration": 1.5, "custom": "value"}]
        result = quantize_notes_to_scale.fn(notes, "C", "major")
        
        self.assertNotIn("error", result)
        quantized = result["notes"][0]
        self.assertEqual(quantized["velocity"], 80)
        self.assertEqual(quantized["duration"], 1.5)
        self.assertEqual(quantized["custom"], "value")
    
    def test_quantize_to_minor_scale(self):
        """Test quantizing to minor scale."""
        notes = [{"pitch": 60}, {"pitch": 63}, {"pitch": 67}]
        result = quantize_notes_to_scale.fn(notes, "A", "minor")
        
        self.assertNotIn("error", result)
        self.assertEqual(len(result["notes"]), 3)
    
    def test_quantize_empty_list(self):
        """Test quantizing empty note list (should return error)."""
        result = quantize_notes_to_scale.fn([], "C", "major")
        
        # Implementation returns error for empty list
        self.assertIn("error", result)
    
    def test_notes_already_in_scale(self):
        """Test that notes already in scale remain unchanged."""
        # C major scale notes
        notes = [{"pitch": 60}, {"pitch": 62}, {"pitch": 64}, {"pitch": 65}]
        result = quantize_notes_to_scale.fn(notes, "C", "major")
        
        self.assertNotIn("error", result)
        quantized = result["notes"]
        for i, note in enumerate(notes):
            self.assertEqual(quantized[i]["pitch"], note["pitch"])
