"""Tests for src/core/schema.py — SongDefinition, TrackDef, ClipDef, MidiNote, CCEvent."""

import dataclasses
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parents[1]))

import pytest
from src.core.schema import SongDefinition, SongMeta, TrackDef, ClipDef, MidiNote, CCEvent


class TestCCEvent:
    """Test CCEvent dataclass (Phase 1: schema)."""

    def test_ccevent_round_trip(self):
        """CCEvent round-trips correctly through dataclass construction."""
        evt = CCEvent(controller=11, time=0.5, value=50)
        assert evt.controller == 11
        assert evt.time == 0.5
        assert evt.value == 50

    def test_ccevent_defaults(self):
        """CCEvent has no default fields — all are required."""
        from dataclasses import fields as dc_fields
        field_names = [f.name for f in dc_fields(CCEvent)]
        no_default = []
        for f in dc_fields(CCEvent):
            if f.default is f.default_factory is dataclasses.MISSING:
                no_default.append(f.name)
        assert "controller" in no_default
        assert "time" in no_default
        assert "value" in no_default

    def test_clipdef_with_midi_cc(self):
        """ClipDef with midi_cc field."""
        cc = [CCEvent(11, 0.0, 50), CCEvent(11, 0.18, 127)]
        clip = ClipDef(position=0.0, length=16.0, name="Test", midi_cc=cc)
        assert len(clip.midi_cc) == 2
        assert clip.midi_cc[0].controller == 11
        assert clip.midi_cc[0].time == 0.0
        assert clip.midi_cc[0].value == 50

    def test_clipdef_midi_cc_default_is_empty(self):
        """ClipDef.midi_cc defaults to empty list."""
        clip = ClipDef(position=0.0, length=16.0, name="Test")
        assert clip.midi_cc == []

    def test_song_validate_empty_midi_cc(self):
        """Song.validate() passes with empty midi_cc (no regression)."""
        meta = SongMeta(bpm=95, key="Am")
        note = MidiNote(pitch=60, start=0.0, duration=1.0, velocity=100)
        clip = ClipDef(position=0.0, length=16.0, name="Test", midi_notes=[note], midi_cc=[])
        track = TrackDef(name="Test", clips=[clip])
        song = SongDefinition(meta=meta, tracks=[track])
        errors = song.validate()
        assert errors == []


class TestSongDefinitionInstantiation:
    """Test SongDefinition instantiation with valid data."""

    def test_song_definition_with_valid_data(self):
        """SongDefinition instantiates with meta and tracks."""
        meta = SongMeta(bpm=95, key="Am", title="Test Song")
        song = SongDefinition(meta=meta, tracks=[])
        assert song.meta.bpm == 95
        assert song.meta.key == "Am"
        assert song.meta.title == "Test Song"
        assert song.tracks == []

    def test_song_definition_with_multiple_tracks(self):
        """SongDefinition accepts multiple TrackDef entries."""
        meta = SongMeta(bpm=95, key="Am")
        track1 = TrackDef(name="Kick", volume=0.85)
        track2 = TrackDef(name="Bass", volume=0.80)
        song = SongDefinition(meta=meta, tracks=[track1, track2])
        assert len(song.tracks) == 2
        assert song.tracks[0].name == "Kick"
        assert song.tracks[1].name == "Bass"


class TestTrackDefWithAudioClip:
    """Test TrackDef with audio clip (sample_path)."""

    def test_track_with_audio_clip(self):
        """TrackDef with audio clip has audio_path set."""
        clip = ClipDef(
            position=0.0,
            length=16.0,
            name="Kick Loop",
            audio_path="C:/samples/kick.wav",
        )
        track = TrackDef(name="Drums", clips=[clip])
        assert len(track.clips) == 1
        assert track.clips[0].is_audio
        assert track.clips[0].audio_path == "C:/samples/kick.wav"
        assert not track.clips[0].is_midi

    def test_track_with_multiple_audio_clips(self):
        """TrackDef can hold multiple audio clips."""
        clip1 = ClipDef(position=0.0, length=16.0, audio_path="C:/samples/kick.wav")
        clip2 = ClipDef(position=16.0, length=16.0, audio_path="C:/samples/kick2.wav")
        track = TrackDef(name="Drums", clips=[clip1, clip2])
        assert len(track.clips) == 2
        assert track.clips[0].audio_path == "C:/samples/kick.wav"
        assert track.clips[1].audio_path == "C:/samples/kick2.wav"


class TestClipDefWithMidiNotes:
    """Test ClipDef with MIDI notes."""

    def test_clip_with_midi_notes(self):
        """ClipDef with midi_notes is identified as MIDI clip."""
        note = MidiNote(pitch=36, start=0.0, duration=1.0, velocity=100)
        clip = ClipDef(
            position=0.0,
            length=16.0,
            name="Kick Pattern",
            midi_notes=[note],
        )
        assert clip.is_midi
        assert not clip.is_audio
        assert len(clip.midi_notes) == 1
        assert clip.midi_notes[0].pitch == 36

    def test_clip_with_multiple_midi_notes(self):
        """ClipDef can hold multiple MidiNote entries."""
        notes = [
            MidiNote(pitch=36, start=0.0, duration=0.25, velocity=115),
            MidiNote(pitch=36, start=1.5, duration=0.25, velocity=105),
            MidiNote(pitch=38, start=2.0, duration=0.15, velocity=100),
        ]
        clip = ClipDef(position=0.0, length=16.0, name="Drum Pattern", midi_notes=notes)
        assert len(clip.midi_notes) == 3
        assert clip.midi_notes[0].pitch == 36
        assert clip.midi_notes[1].pitch == 36
        assert clip.midi_notes[2].pitch == 38


class TestValidationNegativeBPM:
    """Test validation: negative BPM raises ValueError."""

    def test_negative_bpm_raises_value_error(self):
        """SongDefinition.validate() returns error for negative BPM."""
        meta = SongMeta(bpm=-10, key="Am")
        song = SongDefinition(meta=meta, tracks=[])
        errors = song.validate()
        assert any("bpm" in e.lower() for e in errors)

    def test_zero_bpm_raises_value_error(self):
        """SongDefinition.validate() returns error for zero BPM."""
        meta = SongMeta(bpm=0, key="Am")
        song = SongDefinition(meta=meta, tracks=[])
        errors = song.validate()
        assert any("bpm" in e.lower() for e in errors)

    def test_valid_bpm_passes(self):
        """SongDefinition.validate() passes for BPM 20-999."""
        meta = SongMeta(bpm=95, key="Am")
        song = SongDefinition(meta=meta, tracks=[])
        errors = song.validate()
        assert not any("bpm" in e.lower() for e in errors)


class TestMidiNote:
    """Test MidiNote dataclass."""

    def test_midi_note_defaults(self):
        """MidiNote has sensible defaults for velocity."""
        note = MidiNote(pitch=60, start=0.0, duration=1.0)
        assert note.pitch == 60
        assert note.start == 0.0
        assert note.duration == 1.0
        assert note.velocity == 64  # default

    def test_midi_note_explicit_velocity(self):
        """MidiNote accepts explicit velocity."""
        note = MidiNote(pitch=60, start=0.0, duration=1.0, velocity=127)
        assert note.velocity == 127

    def test_midi_note_velocity_clamping(self):
        """MidiNote does NOT clamp — accepts any int (caller's responsibility)."""
        note = MidiNote(pitch=60, start=0.0, duration=1.0, velocity=200)
        assert note.velocity == 200


class TestClipDefVolMult:
    """Test ClipDef.vol_mult default and behavior."""

    def test_vol_mult_default_is_one(self):
        """ClipDef.vol_mult defaults to 1.0."""
        clip = ClipDef(position=0.0, length=16.0, name="Test")
        assert clip.vol_mult == 1.0

    def test_vol_mult_custom_value(self):
        """ClipDef.vol_mult accepts custom value."""
        clip = ClipDef(position=0.0, length=16.0, name="Test", vol_mult=0.7)
        assert clip.vol_mult == 0.7

    def test_audio_clip_vol_mult_default_is_one(self):
        """Audio clip with default vol_mult=1.0 has no volume effect."""
        clip = ClipDef(position=0.0, length=16.0, audio_path="test.wav", vol_mult=1.0)
        assert clip.is_audio
        assert clip.vol_mult == 1.0

    def test_midi_clip_vol_mult_default_is_one(self):
        """MIDI clip with default vol_mult=1.0 has no velocity scaling."""
        note = MidiNote(pitch=60, start=0.0, duration=1.0, velocity=100)
        clip = ClipDef(position=0.0, length=16.0, midi_notes=[note], vol_mult=1.0)
        assert clip.is_midi
        assert clip.vol_mult == 1.0
