"""Integration tests for scripts/compose.py — drumloop-first compose workflow."""

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

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

import pytest
from src.core.schema import SongDefinition, SongMeta, TrackDef, ClipDef, MidiNote, CCEvent
from src.reaper_builder import RPPBuilder
from src.composer.drum_analyzer import DrumLoopAnalysis, Transient, BeatGrid


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _fake_analysis():
    return DrumLoopAnalysis(
        file_path="fake_drumloop.wav",
        bpm=95.0,
        duration=8.0,
        beats=[0.0, 0.6316, 1.2632, 1.8947, 2.5263, 3.1579, 3.7895, 4.4211],
        transients=[
            Transient(time=0.0, type="kick", energy=0.8, spectral_centroid=100),
            Transient(time=0.6316, type="hihat", energy=0.4, spectral_centroid=8000),
            Transient(time=1.2632, type="snare", energy=0.7, spectral_centroid=3000),
            Transient(time=1.8947, type="hihat", energy=0.3, spectral_centroid=7000),
            Transient(time=2.5263, type="kick", energy=0.8, spectral_centroid=100),
            Transient(time=3.1579, type="snare", energy=0.6, spectral_centroid=3500),
            Transient(time=3.7895, type="hihat", energy=0.4, spectral_centroid=9000),
        ],
        beat_grid=BeatGrid(
            quarter=[0.0, 0.6316, 1.2632, 1.8947, 2.5263, 3.1579, 3.7895, 4.4211],
            eighth=[i * 0.3158 for i in range(16)],
            sixteenth=[i * 0.1579 for i in range(32)],
        ),
        key="Am",
        key_confidence=0.85,
        energy_profile=[0.8, 0.4, 0.7, 0.3, 0.8, 0.6, 0.4, 0.3],
        bar_count=2,
        sample_rate=44100,
    )


def _mock_main(tmp_path, extra_args=None):
    output = tmp_path / "track.rpp"
    fake_analysis = _fake_analysis()

    with patch("scripts.compose.SampleSelector") as mock_cls:
        mock_sel = MagicMock()
        mock_sel._samples = [
            {
                "role": "drumloop",
                "perceptual": {"tempo": 95.0},
                "musical": {"key": "Am", "mode": "minor"},
                "character": "dark",
                "original_path": "fake_drumloop.wav",
                "original_name": "fake_drumloop.wav",
                "file_hash": "abc123",
            },
            {
                "role": "snare",
                "perceptual": {"tempo": 0},
                "musical": {"key": "X"},
                "character": "sharp",
                "original_path": "fake_clap.wav",
                "original_name": "fake_clap.wav",
                "file_hash": "clap123",
            },
            {
                "role": "vocal",
                "perceptual": {"tempo": 95.0},
                "musical": {"key": "Am", "mode": "minor"},
                "character": "melodic",
                "original_path": "fake_vocal.wav",
                "original_name": "fake_vocal.wav",
                "file_hash": "vox123",
            },
        ]
        mock_sel.select.return_value = [
            MagicMock(sample={
                "original_path": "fake_clap.wav",
                "file_hash": "clap123",
            }),
        ]
        mock_sel.select_diverse.return_value = [
            {
                "original_path": "fake_vocal.wav",
                "file_hash": "vox123",
            },
        ]
        mock_cls.return_value = mock_sel

        from scripts.compose import main
        original_argv = sys.argv
        try:
            argv = ["compose", "--output", str(output)]
            if extra_args:
                argv.extend(extra_args)
            sys.argv = argv
            main()
        finally:
            sys.argv = original_argv

    return output


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

class TestComposeRppOutput:

    def test_compose_produces_rpp_file(self, tmp_path):
        output = _mock_main(tmp_path)
        assert output.exists(), f"Expected {output} to exist"

    def test_compose_rpp_has_min_6_tracks(self, tmp_path):
        output = _mock_main(tmp_path)
        content = output.read_text(encoding="utf-8")
        track_count = content.count("<TRACK")
        assert track_count >= 6, f"Expected >= 6 tracks, got {track_count}"

    def test_compose_has_fxchain(self, tmp_path):
        output = _mock_main(tmp_path)
        content = output.read_text(encoding="utf-8")
        assert "FXCHAIN" in content, "Expected FXCHAIN in output"

    def test_compose_has_midi_source(self, tmp_path):
        output = _mock_main(tmp_path)
        content = output.read_text(encoding="utf-8")
        assert "SOURCE MIDI" in content, "Expected MIDI source in output"

    def test_compose_has_audio_source(self, tmp_path):
        output = _mock_main(tmp_path)
        content = output.read_text(encoding="utf-8")
        assert "SOURCE WAVE" in content, "Expected WAVE source in output"

    def test_compose_invalid_bpm_raises(self, tmp_path):
        with pytest.raises(ValueError, match="bpm must be > 0"):
            _mock_main(tmp_path, ["--bpm", "0"])

    def test_compose_negative_bpm_raises(self, tmp_path):
        with pytest.raises(ValueError, match="bpm must be > 0"):
            _mock_main(tmp_path, ["--bpm", "-10"])


class TestDrumloopFirstTracks:

    def test_all_tracks_created(self, tmp_path):
        output = _mock_main(tmp_path)
        content = output.read_text(encoding="utf-8")
        for name in ("Drumloop", "808 Bass", "Chords", "Lead", "Clap", "Pad", "Reverb", "Delay"):
            assert name in content, f"Expected track '{name}' in output"

    def test_clap_on_dembow_beats(self, tmp_path):
        from scripts.compose import build_clap_track, SECTIONS
        from src.core.schema import SectionDef

        sections = [SectionDef(name="chorus", bars=4, energy=1.0)]
        offsets = [0.0]

        mock_selector = MagicMock()
        mock_selector.select.return_value = [
            MagicMock(sample={"original_path": "clap.wav"}),
        ]

        track = build_clap_track(mock_selector, sections, offsets)
        positions = [c.position for c in track.clips]
        assert 2.0 in positions, "Clap on beat 2 (backbeat)"
        assert 3.5 in positions, "Clap on beat 3.5 (dembow)"

    def test_bass_uses_kick_free_zones(self):
        from scripts.compose import build_bass_track
        from src.core.schema import SectionDef

        analysis = _fake_analysis()
        sections = [SectionDef(name="verse", bars=4, energy=1.0)]
        offsets = [0.0]

        track = build_bass_track(sections, offsets, "A", True)
        assert len(track.clips) > 0, "Bass should have clips"
        assert all(n.duration == 1.5 for n in track.clips[0].midi_notes), "Bass notes should be 1.5 beats (808 pattern)"

    def test_chords_change_on_downbeats(self):
        from scripts.compose import build_chords_track
        from src.core.schema import SectionDef

        analysis = _fake_analysis()
        sections = [SectionDef(name="verse", bars=8, energy=1.0)]
        offsets = [0.0]

        track = build_chords_track(sections, offsets, "A", True)
        starts = sorted(set(n.start for n in track.clips[0].midi_notes))
        for s in starts:
            assert s % 4.0 == 0.0, f"Chord change at beat {s} — should be on downbeat"

    def test_melody_uses_hook_structure(self):
        """Lead melody should use hook-based call-response from melody_engine."""
        from scripts.compose import build_melody_track, get_pentatonic
        from src.core.schema import SectionDef
        from src.composer.melody_engine import _resolve_chord_tones

        sections = [SectionDef(name="chorus", bars=4, energy=1.0)]
        offsets = [0.0]

        track = build_melody_track(sections, offsets, "A", True, seed=42)
        assert len(track.clips) > 0, "Melody should have clips"

        notes = track.clips[0].midi_notes
        pitches = {n.pitch for n in notes}
        assert len(pitches) > 1, "Melody should use multiple notes"

        # Verify chord-tone emphasis: quarter-position notes should favor chord tones
        penta = get_pentatonic("A", True, 4) + get_pentatonic("A", True, 5)
        chord_tones = _resolve_chord_tones("A", True, 0, 4)

        quarter_notes = [n for n in notes if abs(n.start % 1.0) < 0.001]
        if quarter_notes:
            chord_count = sum(
                1 for n in quarter_notes
                if any(abs(n.pitch - ct) % 12 == 0 for ct in chord_tones)
            )
            ratio = chord_count / len(quarter_notes)
            assert ratio >= 0.5, (
                f"Hook chord tone ratio {ratio:.1%} below 50%"
            )

        # Verify notes span the section length
        max_end = max(n.start + n.duration for n in notes)
        assert max_end <= 16.0 + 0.001, "Notes should fit within 4 bars"

    def test_master_chain_present(self, tmp_path):
        output = _mock_main(tmp_path)
        content = output.read_text(encoding="utf-8")
        # After calibration, master chain uses Ozone 12 triplet (with spaces in RPP)
        assert "Ozone 12" in content or "Pro-Q" in content, (
            "Expected Ozone 12 or Pro-Q in master chain"
        )

    def test_sends_wired(self, tmp_path):
        output = _mock_main(tmp_path)
        content = output.read_text(encoding="utf-8")
        assert "AUXRECV" in content, "Expected send routing in output"


class TestBackwardCompat:

    def test_imports_exist(self):
        from scripts.compose import (
            build_section_tracks, create_return_tracks, EFFECT_ALIASES,
            build_fx_chain, build_sampler_plugin,
        )
        assert callable(build_section_tracks)
        assert callable(create_return_tracks)
        assert callable(build_fx_chain)
        assert callable(build_sampler_plugin)
        assert isinstance(EFFECT_ALIASES, dict)

    def test_create_return_tracks(self):
        from scripts.compose import create_return_tracks
        tracks = create_return_tracks()
        assert len(tracks) == 2
        assert tracks[0].name == "Reverb"
        assert tracks[1].name == "Delay"
        assert len(tracks[0].plugins) > 0
        assert len(tracks[1].plugins) > 0


class TestSectionEnergy:
    """Integration tests for section energy curve — sparse vs dense sections."""

    def test_section_rename_pre_chorus_not_build(self):
        """SECTIONS uses 'pre-chorus' not 'build'."""
        from scripts.compose import SECTIONS
        names = {name for name, _, _, _ in SECTIONS}
        assert "pre-chorus" in names, "Expected 'pre-chorus' section"
        assert "build" not in names, "'build' must be renamed to 'pre-chorus'"

    def test_intro_has_no_bass(self):
        """Intro section should NOT have bass (sparse)."""
        from scripts.compose import build_bass_track, build_section_structure
        sections, offsets = build_section_structure()
        intro_section = sections[0]
        assert intro_section.name == "intro"
        track = build_bass_track(sections, offsets, "A", True)
        # Find clips whose position matches the intro offset
        intro_positions = [c.position for c in track.clips if c.position == offsets[0] * 4.0]
        assert len(intro_positions) == 0, "Intro should have no bass clips"

    def test_intro_has_no_chords(self):
        """Intro section should NOT have chords (sparse)."""
        from scripts.compose import build_chords_track, build_section_structure
        sections, offsets = build_section_structure()
        track = build_chords_track(sections, offsets, "A", True)
        intro_positions = [c.position for c in track.clips if c.position == offsets[0] * 4.0]
        assert len(intro_positions) == 0, "Intro should have no chord clips"

    def test_intro_has_no_lead(self):
        """Intro section should NOT have lead (sparse)."""
        from scripts.compose import build_lead_track, build_section_structure
        sections, offsets = build_section_structure()
        track = build_lead_track(sections, offsets, "A", True, seed=42)
        intro_positions = [c.position for c in track.clips if c.position == offsets[0] * 4.0]
        assert len(intro_positions) == 0, "Intro should have no lead clips"

    def test_chorus_has_bass_chords_lead(self):
        """Chorus section should have bass, chords, and lead (full band)."""
        from scripts.compose import (
            build_bass_track, build_chords_track, build_lead_track,
            build_section_structure,
        )
        sections, offsets = build_section_structure()
        # Find the chorus section index (first "chorus")
        chorus_idx = next(i for i, s in enumerate(sections) if s.name == "chorus")
        chorus_offset = offsets[chorus_idx] * 4.0

        bass = build_bass_track(sections, offsets, "A", True)
        chords = build_chords_track(sections, offsets, "A", True)
        lead = build_lead_track(sections, offsets, "A", True, seed=42)

        assert any(c.position == chorus_offset for c in bass.clips), "Chorus should have bass"
        assert any(c.position == chorus_offset for c in chords.clips), "Chorus should have chords"
        assert any(c.position == chorus_offset for c in lead.clips), "Chorus should have lead"

    def test_chorus_clips_have_vol_mult(self):
        """Clips in chorus sections should have vol_mult set from section."""
        from scripts.compose import build_drumloop_track, build_bass_track, build_section_structure
        sections, offsets = build_section_structure()
        chorus_idx = next(i for i, s in enumerate(sections) if s.name == "chorus")
        chorus_offset = offsets[chorus_idx] * 4.0

        drumloop_track = build_drumloop_track(sections, offsets, seed=0)
        bass_track = build_bass_track(sections, offsets, "A", True)

        # Audio clips get vol_mult
        dl_clips = [c for c in drumloop_track.clips if c.position == chorus_offset]
        if dl_clips:
            assert dl_clips[0].vol_mult == 1.0, "Chorus drumloop vol_mult should be 1.0"

        # MIDI clips get vol_mult
        bass_clips = [c for c in bass_track.clips if c.position == chorus_offset]
        if bass_clips:
            assert bass_clips[0].vol_mult == 1.0, "Chorus bass vol_mult should be 1.0"

    def test_velocity_scaled_in_intro_vs_chorus(self):
        """Verse has lower velocity notes than chorus (velocity_mult 0.7 vs 1.0)."""
        from scripts.compose import build_bass_track, build_section_structure
        sections, offsets = build_section_structure()
        verse_idx = next(i for i, s in enumerate(sections) if s.name == "verse")
        verse_offset = offsets[verse_idx] * 4.0
        chorus_idx = next(i for i, s in enumerate(sections) if s.name == "chorus")
        chorus_offset = offsets[chorus_idx] * 4.0

        track = build_bass_track(sections, offsets, "A", True)
        verse_clip = next(c for c in track.clips if c.position == verse_offset)
        chorus_clip = next(c for c in track.clips if c.position == chorus_offset)

        verse_vel = verse_clip.midi_notes[0].velocity if verse_clip.midi_notes else 0
        chorus_vel = chorus_clip.midi_notes[0].velocity if chorus_clip.midi_notes else 0
        assert verse_vel < chorus_vel, \
            f"Verse velocity ({verse_vel}) should be less than chorus ({chorus_vel})"

    def test_drumloop_assignments_no_break_key(self):
        """DRUMLOOP_ASSIGNMENTS has no 'break' key — replaced by activity matrix."""
        from scripts.compose import DRUMLOOP_ASSIGNMENTS
        assert "break" not in DRUMLOOP_ASSIGNMENTS
        assert "pre-chorus" in DRUMLOOP_ASSIGNMENTS


class TestSidechainBassCC:
    """Integration tests for CC11 sidechain ducking on 808 bass."""

    def test_bass_track_populates_midi_cc_with_kick_cache(self):
        """build_bass_track populates midi_cc when kick cache present."""
        from scripts.compose import build_bass_track
        from src.core.schema import SectionDef

        sections = [SectionDef(name="verse", bars=8, energy=0.5, velocity_mult=0.7)]
        offsets = [0.0]
        kick_cache = {"fake_drumloop.wav": [1.0, 3.0, 5.0, 7.0]}

        track = build_bass_track(sections, offsets, "A", True, kick_cache=kick_cache)
        assert len(track.clips) > 0, "Bass should have clips"

        verse_clip = track.clips[0]
        # With 4 kicks in range, each generates 3 CC events
        assert len(verse_clip.midi_cc) == 12, f"Expected 12 CC events (4 kicks × 3), got {len(verse_clip.midi_cc)}"

        # Check first kick's duck events
        cc_times = [(cc.time, cc.value) for cc in verse_clip.midi_cc[:3]]
        assert (1.0, 50) in cc_times, f"Expected CC dip at 1.0, got {cc_times}"
        assert (1.02, 50) in cc_times, f"Expected CC hold at 1.02, got {cc_times}"
        assert (1.18, 127) in cc_times, f"Expected CC release at 1.18, got {cc_times}"

    def test_bass_track_no_kick_cache_empty_cc(self):
        """build_bass_track produces empty midi_cc when no kick cache provided."""
        from scripts.compose import build_bass_track
        from src.core.schema import SectionDef

        sections = [SectionDef(name="verse", bars=8, energy=0.5, velocity_mult=0.7)]
        offsets = [0.0]

        track = build_bass_track(sections, offsets, "A", True)
        assert len(track.clips) > 0
        verse_clip = track.clips[0]
        assert verse_clip.midi_cc == [], "midi_cc should be empty without kick cache"

    def test_bass_track_no_kicks_in_range(self):
        """build_bass_track produces empty midi_cc when no kicks in clip range."""
        from scripts.compose import build_bass_track
        from src.core.schema import SectionDef

        sections = [SectionDef(name="verse", bars=2, energy=0.5, velocity_mult=0.7)]
        offsets = [0.0]
        # Kicks at beats far outside the clip range (0-8 beats)
        kick_cache = {"fake_drumloop.wav": [100.0, 200.0]}

        track = build_bass_track(sections, offsets, "A", True, kick_cache=kick_cache)
        verse_clip = track.clips[0]
        assert verse_clip.midi_cc == [], "midi_cc should be empty when kicks are outside clip range"

    def test_bass_track_preserves_notes_with_cc(self):
        """build_bass_track preserves existing note generation when CC added."""
        from scripts.compose import build_bass_track
        from src.core.schema import SectionDef

        sections = [SectionDef(name="verse", bars=4, energy=0.5, velocity_mult=0.7)]
        offsets = [0.0]
        kick_cache = {"fake_drumloop.wav": [2.0]}

        track = build_bass_track(sections, offsets, "A", True, kick_cache=kick_cache)
        verse_clip = track.clips[0]
        # Should still have bass notes (i - iv pattern for 4 bars)
        assert len(verse_clip.midi_notes) > 0, "Bass notes should still be generated"
        assert len(verse_clip.midi_cc) == 3, "Should have 3 CC events for 1 kick in range"

    def test_bass_track_kicks_relative_to_clip(self):
        """build_bass_track produces CC times relative to clip start, not absolute."""
        from scripts.compose import build_bass_track
        from src.core.schema import SectionDef

        # Section at offset 2 bars (8 beats)
        sections = [SectionDef(name="verse", bars=8, energy=0.5, velocity_mult=0.7)]
        offsets = [2.0]  # starts at bar 2 = beat 8

        # Kick at absolute beat 9.0 → relative beat 1.0
        kick_cache = {"fake_drumloop.wav": [9.0]}

        track = build_bass_track(sections, offsets, "A", True, kick_cache=kick_cache)
        verse_clip = track.clips[0]
        assert len(verse_clip.midi_cc) == 3

        # CC times should be relative to clip start (9.0 - 8.0 = 1.0)
        first_cc_time = verse_clip.midi_cc[0].time
        assert first_cc_time == 1.0, f"CC time should be 1.0 (relative), got {first_cc_time}"
