import sys
import tempfile
from pathlib import Path
import unittest
import wave

ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "src"

if str(SRC) not in sys.path:
    sys.path.insert(0, str(SRC))

from bpm_finder import (  # noqa: E402
    analyze_audio_file,
    bpm_to_ms,
    estimate_bpm_from_taps,
    is_bpm_in_range,
    ms_to_bpm,
    normalize_bpm,
    tempo_bucket,
)


def write_click_track(path: Path, bpm: float, beats: int = 8, sample_rate: int = 44100) -> None:
    beat_interval_seconds = 60.0 / bpm
    total_duration_seconds = (beats + 1) * beat_interval_seconds
    total_frames = int(total_duration_seconds * sample_rate)
    samples = [0] * total_frames

    click_width = max(1, sample_rate // 200)
    for beat_index in range(beats):
        start = int(beat_index * beat_interval_seconds * sample_rate)
        end = min(total_frames, start + click_width)
        for frame_index in range(start, end):
            samples[frame_index] = 26000

    with wave.open(str(path), "wb") as wav_file:
        wav_file.setnchannels(1)
        wav_file.setsampwidth(2)
        wav_file.setframerate(sample_rate)
        wav_file.writeframes(b"".join(int(sample).to_bytes(2, "little", signed=True) for sample in samples))


class ConversionTests(unittest.TestCase):
    def test_bpm_to_ms_for_quarter_notes(self) -> None:
        self.assertEqual(bpm_to_ms(128, note_division=4), 468.75)

    def test_ms_to_bpm_for_quarter_notes(self) -> None:
        self.assertEqual(ms_to_bpm(468.75, note_division=4), 128.0)


class TapTempoTests(unittest.TestCase):
    def test_estimate_bpm_from_taps_uses_median_strategy_by_default(self) -> None:
        timestamps = [0, 500, 1000, 1500, 2000]
        self.assertEqual(estimate_bpm_from_taps(timestamps), 120.0)

    def test_estimate_bpm_from_taps_rejects_unsorted_timestamps(self) -> None:
        with self.assertRaises(ValueError):
            estimate_bpm_from_taps([0, 500, 490])


class AudioAnalysisTests(unittest.TestCase):
    def test_analyze_audio_file_detects_click_track_bpm(self) -> None:
        with tempfile.TemporaryDirectory() as temp_dir:
            wav_path = Path(temp_dir) / "click-track.wav"
            write_click_track(wav_path, bpm=120.0)

            detected_bpm = analyze_audio_file(wav_path)

        self.assertAlmostEqual(detected_bpm, 120.0, delta=1.0)

    def test_analyze_audio_file_rejects_missing_files(self) -> None:
        with self.assertRaises(ValueError):
            analyze_audio_file("missing.wav")


class RangeTests(unittest.TestCase):
    def test_normalize_bpm_promotes_half_time_values(self) -> None:
        self.assertEqual(normalize_bpm(64), 128.0)

    def test_is_bpm_in_range_is_inclusive(self) -> None:
        self.assertTrue(is_bpm_in_range(128, 90, 180))
        self.assertFalse(is_bpm_in_range(181, 90, 180))

    def test_tempo_bucket_returns_expected_label(self) -> None:
        self.assertEqual(tempo_bucket(174), "fast")


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