import { describe, expect, it } from 'vitest';
import { renderMusic, renderMusicStereo, type MusicSpec } from '../Music';

const RATE = 22050;
const spb = (bpm: number): number => 60 / bpm;

const same = (a: Float32Array, b: Float32Array): boolean =>
    a.length === b.length && a.every((v, i) => v === b[i]);

const peak = (samples: Float32Array): number => Math.max(...[...samples].map(Math.abs));

/** Одна нота на долю — простейшая партитура. */
const one = (over: Partial<MusicSpec> = {}): MusicSpec => ({
    bpm: 120,
    beats: 4,
    tracks: [{ instrument: 'pluck', notes: [{ beat: 0, dur: 1, midi: 69 }] }],
    ...over
});

describe('renderMusic', () => {
    it('детерминизм: одинаковые спеки → одинаковые сэмплы', () => {
        expect(same(renderMusic(one()), renderMusic(one()))).toBe(true);
    });

    it('длина лупа — сырая минус кроссфейд (фейд = min(0.5с, 25% длины))', () => {
        const t1 = 4 * spb(120) * RATE; // 2 с
        expect(renderMusic(one({ bpm: 120, beats: 4 })).length).toBe(t1 - 0.5 * RATE);
        const t2 = 8 * spb(60) * RATE; // 8 с — фейд упёрся в 0.5 с
        expect(renderMusic(one({ bpm: 60, beats: 8 })).length).toBe(t2 - 0.5 * RATE);
    });

    it('высота ноты доезжает: другой midi — другие сэмплы', () => {
        const a = renderMusic(one());
        const b = renderMusic(one({ tracks: [{ instrument: 'pluck', notes: [{ beat: 0, dur: 1, midi: 71 }] }] }));
        expect(same(a, b)).toBe(false);
    });

    it('vel и gain меняют микс (пик после нормализации — 0.8 вне кроссфейд-зоны)', () => {
        const beat1 = one({ tracks: [{ instrument: 'pluck', notes: [{ beat: 1, dur: 1, midi: 69 }] }] });
        expect(peak(renderMusic(beat1))).toBeCloseTo(0.8, 2); // нормализация

        // gain одной партии при опорной второй стереть нормализацией нельзя:
        const mix = (g: number): MusicSpec => ({
            bpm: 120,
            beats: 4,
            tracks: [
                { instrument: 'pad', notes: [{ beat: 0, dur: 4, midi: 57 }] },
                { instrument: 'pluck', notes: [{ beat: 1, dur: 1, midi: 69 }], gain: g }
            ]
        });
        expect(same(renderMusic(mix(1)), renderMusic(mix(0.3)))).toBe(false);
    });

    it('ноты за краем лупа обрезаются, пустой трек — тишина нормализована в 0', () => {
        const empty = renderMusic(one({ tracks: [{ instrument: 'pad', notes: [] }] }));
        expect(peak(empty)).toBe(0);

        const clipped = renderMusic(one({ beats: 1, tracks: [{ instrument: 'pluck', notes: [{ beat: 0.5, dur: 2, midi: 69 }] }] }));
        expect(clipped.length).toBeLessThanOrEqual(1 * spb(120) * RATE);
        expect(peak(clipped)).toBeGreaterThan(0);
    });

    it('стерео: interleaved L,R; pan 0 — каналы равны, pan -1 — громче слева', () => {
        const frames = 4 * spb(120) * RATE;
        const spec = one({ tracks: [{ instrument: 'pluck', notes: [{ beat: 0, dur: 1, midi: 69 }], pan: -1 }] });
        const s = renderMusicStereo(spec);
        expect(s.length).toBe(frames * 2);

        // Пики каналов (вне кроссфейд-зоны начала).
        let lPeak = 0;
        let rPeak = 0;
        for (let i = 0; i < frames; i++) {
            lPeak = Math.max(lPeak, Math.abs(s[i * 2]!));
            rPeak = Math.max(rPeak, Math.abs(s[i * 2 + 1]!));
        }
        expect(lPeak).toBeGreaterThan(rPeak * 2); // панорама влево

        const centered = renderMusicStereo(one());
        let equal = true;
        for (let i = 0; i < centered.length; i += 2) {
            if (centered[i] !== centered[i + 1]) equal = false;
        }
        expect(equal).toBe(true); // pan 0 — моно-копия в обоих каналах
    });

    it('стерео: нормализация по общему пику каналов — баланс pan не искажается', () => {
        // Тихий левый + громкий правый: пик пары равен 0.8 (нота вне кроссфейд-зоны).
        const spec = one({
            tracks: [
                { instrument: 'pluck', notes: [{ beat: 1, dur: 1, midi: 69 }], pan: -1 },
                { instrument: 'pluck', notes: [{ beat: 1, dur: 1, midi: 69 }], pan: 1, gain: 2 }
            ]
        });
        const s = renderMusicStereo(spec);
        expect(peak(s)).toBeCloseTo(0.8, 2);
    });
});