import tempfile
import struct
import math
from pathlib import Path


def create_test_wav_file(name, content=None):
    """Create a temporary WAV file for testing. Name is preserved."""
    tmpdir = tempfile.gettempdir()
    path = str(Path(tmpdir) / name)
    if content is None:
        sr = 44100
        bits = 16
        channels = 1
        duration = 0.5
        num_samples = int(sr * duration)

        samples = []
        for i in range(num_samples):
            val = int(math.sin(2 * math.pi * 440 * i / sr) * 0.3 * 32767)
            samples.append(struct.pack('<h', val))

        data = b''.join(samples)
        data_size = len(data)
        file_size = 36 + data_size

        buf = bytearray()
        buf += b'RIFF'
        buf += struct.pack('<I', file_size)
        buf += b'WAVE'
        buf += b'fmt '
        buf += struct.pack('<I', 16)
        buf += struct.pack('<H', 1)
        buf += struct.pack('<H', channels)
        buf += struct.pack('<I', sr)
        buf += struct.pack('<I', sr * channels * bits // 8)
        buf += struct.pack('<H', channels * bits // 8)
        buf += struct.pack('<H', bits)
        buf += b'data'
        buf += struct.pack('<I', data_size)
        buf += data
        content = bytes(buf)

    with open(path, 'wb') as f:
        f.write(content)
    return path


def cleanup_test_wav_file(path):
    """Clean up temporary WAV file."""
    try:
        Path(path).unlink()
    except FileNotFoundError:
        pass


class TestFrontend:
    """Playwright interaction tests for Kitmaker UI."""

    def test_page_loads(self, page):
        header = page.locator('h1')
        assert header.text_content() == 'Razzmatazz Kitmaker'

        ping = page.locator('.empty-card')
        assert ping.is_visible()

    def test_pad_grid_renders(self, page):
        pads = page.locator('.pad')
        assert pads.count() == 8

    def test_preset_dropdown_populated(self, page):
        select = page.locator('#genrePreset')
        options = select.locator('option')
        texts = [options.nth(i).text_content() for i in range(options.count())]
        assert '— Select —' in texts
        for preset in ['808', '909', 'House', 'Techno', 'Hip-Hop']:
            assert preset in texts

    def test_upload_single_file(self, page, sample_wav):
        name, data = sample_wav

        temp_file = create_test_wav_file(name, data)

        try:
            with page.expect_file_chooser() as fc_info:
                page.locator('#fileBtn').click()
            file_chooser = fc_info.value
            file_chooser.set_files([temp_file])

            page.locator('.sample-card').wait_for(timeout=5000)
            assert page.locator('.sample-card').count() >= 1
        finally:
            cleanup_test_wav_file(temp_file)

    def test_folder_upload(self, page, sample_wav, sample_wav_kick):
        import tempfile
        import os
        name1, data1 = sample_wav
        name2, data2 = sample_wav_kick

        tmpdir = tempfile.mkdtemp()
        tmp1 = os.path.join(tmpdir, name1)
        tmp2 = os.path.join(tmpdir, name2)
        with open(tmp1, 'wb') as f:
            f.write(data1)
        with open(tmp2, 'wb') as f:
            f.write(data2)

        try:
            with page.expect_file_chooser() as fc_info:
                page.locator('#folderBtn').click()
            file_chooser = fc_info.value
            file_chooser.set_files([tmpdir])

            page.locator('.sample-card').first.wait_for(timeout=5000)
            page.wait_for_timeout(500)
            assert page.locator('.sample-card').count() == 2
            count_text = page.locator('#sampleCount').text_content()
            assert '2 samples' in count_text
        finally:
            import shutil
            shutil.rmtree(tmpdir, ignore_errors=True)

    def test_search_filter(self, page, sample_wav, sample_wav_kick):
        name1, data1 = sample_wav
        name2, data2 = sample_wav_kick

        temp_file1 = create_test_wav_file(name1, data1)
        temp_file2 = create_test_wav_file(name2, data2)

        try:
            with page.expect_file_chooser() as fc_info:
                page.locator('#fileBtn').click()
            file_chooser = fc_info.value
            file_chooser.set_files([temp_file1, temp_file2])

            page.locator('.sample-card').first.wait_for(timeout=5000)
            page.wait_for_timeout(500)
            assert page.locator('.sample-card').count() == 2

            page.locator('#searchInput').fill('kick')
            page.wait_for_timeout(200)
            assert page.locator('.sample-card').count() == 1
        finally:
            cleanup_test_wav_file(temp_file1)
            cleanup_test_wav_file(temp_file2)

    def test_assign_sample_to_pad_by_click(self, page, sample_wav):
        name, data = sample_wav

        temp_file = create_test_wav_file(name, data)
        try:
            with page.expect_file_chooser() as fc_info:
                page.locator('#fileBtn').click()
            file_chooser = fc_info.value
            file_chooser.set_files([temp_file])
            page.wait_for_timeout(500)

            sel = page.locator('.sel').first
            sel.click()
            page.wait_for_timeout(300)

            filled_pads = page.locator('.pad.filled')
            assert filled_pads.count() >= 1
            assert page.locator('.sel.on').count() >= 1
        finally:
            cleanup_test_wav_file(temp_file)

    def test_remove_sample_from_pad(self, page, sample_wav):
        name, data = sample_wav

        temp_file = create_test_wav_file(name, data)
        try:
            with page.expect_file_chooser() as fc_info:
                page.locator('#fileBtn').click()
            file_chooser = fc_info.value
            file_chooser.set_files([temp_file])
            page.wait_for_timeout(500)

            page.locator('.sel').first.click()
            page.wait_for_timeout(300)

            page.locator('.remove').first.click()
            page.wait_for_timeout(200)
            assert page.locator('.pad.filled').count() == 0
        finally:
            cleanup_test_wav_file(temp_file)

    def test_apply_preset(self, page):
        page.locator('#genrePreset').select_option('Techno')
        page.wait_for_timeout(200)

        pad_types = page.evaluate('() => pads.map(p => p.type)')
        expected = ['kick', 'clap', 'closedHat', 'openHat', 'tom', 'rim', 'perc', 'slice']
        assert pad_types == expected, f'Pad types: {pad_types}'

    def test_analyze_single_sample(self, page, sample_wav):
        name, data = sample_wav

        temp_file = create_test_wav_file(name, data)
        try:
            with page.expect_file_chooser() as fc_info:
                page.locator('#fileBtn').click()
            file_chooser = fc_info.value
            file_chooser.set_files([temp_file])
            page.wait_for_timeout(500)

            analyze_btn = page.locator('.analyze-btn').first
            if analyze_btn.is_visible():
                analyze_btn.click()
                page.wait_for_timeout(3000)
        finally:
            cleanup_test_wav_file(temp_file)

    def test_upload_and_assign_drag_drop(self, page, sample_wav):
        name, data = sample_wav

        temp_file = create_test_wav_file(name, data)
        try:
            with page.expect_file_chooser() as fc_info:
                page.locator('#fileBtn').click()
            file_chooser = fc_info.value
            file_chooser.set_files([temp_file])
            page.wait_for_timeout(500)

            page.locator('.sel').first.click()
            page.wait_for_timeout(300)
            assert page.locator('.pad.filled').count() >= 1
        finally:
            cleanup_test_wav_file(temp_file)

    def test_change_pad_type(self, page, sample_wav):
        name, data = sample_wav

        temp_file = create_test_wav_file(name, data)
        try:
            with page.expect_file_chooser() as fc_info:
                page.locator('#fileBtn').click()
            file_chooser = fc_info.value
            file_chooser.set_files([temp_file])
            page.wait_for_timeout(500)

            page.locator('.sel').first.click()
            page.wait_for_timeout(300)

            type_select = page.locator('.pad.filled select').first
            type_select.select_option('snare')
            page.wait_for_timeout(100)
            assert type_select.input_value() == 'snare'
        finally:
            cleanup_test_wav_file(temp_file)


class TestAPI:
    """API-level validation tests."""

    def test_ping(self, server):
        import requests
        resp = requests.get(f'{server}/api/ping')
        assert resp.ok
        assert resp.json() == {'ok': True}

    def test_probe_wav(self, server, sample_wav):
        import requests
        name, data = sample_wav
        resp = requests.post(f'{server}/api/probe', files={'file': (name, data, 'audio/wav')})
        assert resp.ok
        meta = resp.json()
        assert meta['channels'] == 1
        assert meta['sampleRate'] == 44100
        assert meta['bitDepth'] == 16
        assert meta['ext'] == '.wav'
        assert meta['duration'] > 0

    def test_analyze_wav(self, server, sample_wav):
        import requests
        name, data = sample_wav
        resp = requests.post(f'{server}/api/analyze', files={'file': (name, data, 'audio/wav')})
        assert resp.ok
        result = resp.json()
        assert 'bpm' in result
        assert 'sample_type' in result
        assert 'loop_oneshot' in result
        assert 'key' in result
        assert 'pitch' in result
        assert 'loudness_db' in result
        assert 'dc_offset' in result
        assert '_cached' in result

    def test_analyze_caches_result(self, server, sample_wav):
        import requests
        name, data = sample_wav

        resp1 = requests.post(f'{server}/api/analyze', files={'file': (name, data, 'audio/wav')})
        assert resp1.ok

        resp2 = requests.post(f'{server}/api/analyze', files={'file': (name, data, 'audio/wav')})
        assert resp2.ok
        assert resp2.json()['_cached'] is True

    def test_lookup_bulk(self, server, sample_wav):
        import requests
        import hashlib
        name, data = sample_wav

        requests.post(f'{server}/api/analyze', files={'file': (name, data, 'audio/wav')})

        # Server uses cache_key = sha256(sha256(raw) + filename)
        file_hash = hashlib.sha256(data).hexdigest()
        cache_key = hashlib.sha256((file_hash + name).encode()).hexdigest()
        resp = requests.post(f'{server}/api/lookup', json={'hashes': [cache_key]})
        assert resp.ok
        results = resp.json()
        assert cache_key in results

    def test_convert_wav(self, server, sample_wav):
        import requests
        name, data = sample_wav
        resp = requests.post(f'{server}/api/convert', files={'file': (name, data, 'audio/wav')})
        assert resp.ok
        assert resp.headers['content-type'] == 'audio/wav'
        assert len(resp.content) > 44

    def test_export_empty_fails(self, server):
        import requests
        resp = requests.post(f'{server}/api/export', data={
            'kitName': 'TestKit',
            'config': '[]',
        })
        assert resp.status_code == 400
        assert 'No samples' in resp.text

    def test_export_with_samples(self, server, sample_wav):
        import requests
        name, data = sample_wav
        config = [
            {'slotIndex': 0, 'type': 'kick', 'label': 'Kick', 'volume': 0, 'pan': 0, 'filename': name},
        ]
        resp = requests.post(
            f'{server}/api/export',
            data={'kitName': 'TestKit', 'config': __import__('json').dumps(config)},
            files={'files': (name, data, 'audio/wav')},
        )
        assert resp.ok
        assert resp.headers['content-type'] == 'application/zip'
        import zipfile
        import io
        zf = zipfile.ZipFile(io.BytesIO(resp.content))
        names = zf.namelist()
        assert any(n.endswith('.wav') for n in names)
        assert any(n.endswith('.nnr') for n in names)

    def test_unsupported_format_rejected(self, server):
        import requests
        resp = requests.post(f'{server}/api/probe', files={'file': ('test.txt', b'not audio', 'text/plain')})
        assert resp.status_code == 400
