'use strict';

const { test } = require('node:test');
const assert = require('node:assert/strict');

// tuning-utils.js targets browsers (uses window global). Provide a stub so
// the IIFE's final line `window._tunerUtils = {...}` writes into this object.
const window = {};
global.window = window;
require('../../../../plugins/tuner/utils/tuning-utils.js');
const { freqToMidi, midiToFreq, midiToNote, offsetsToFreqs, getTuningName, preferFlats } = window._tunerUtils;

// ── preferFlats ──────────────────────────────────────────────────────────────

test('preferFlats: returns true for "Eb Standard"', () => {
    assert.equal(preferFlats('Eb Standard'), true);
});

test('preferFlats: returns true for "Bb Standard"', () => {
    assert.equal(preferFlats('Bb Standard'), true);
});

test('preferFlats: returns true for "Ab Standard"', () => {
    assert.equal(preferFlats('Ab Standard'), true);
});

test('preferFlats: returns true for "Db Standard"', () => {
    assert.equal(preferFlats('Db Standard'), true);
});

test('preferFlats: returns false for "Standard"', () => {
    assert.equal(preferFlats('Standard'), false);
});

test('preferFlats: returns false for "Drop D"', () => {
    assert.equal(preferFlats('Drop D'), false);
});

test('preferFlats: returns false for "D Standard"', () => {
    assert.equal(preferFlats('D Standard'), false);
});

test('preferFlats: returns false for "Drop C#"', () => {
    assert.equal(preferFlats('Drop C#'), false);
});

test('preferFlats: returns false for null', () => {
    assert.equal(preferFlats(null), false);
});

test('preferFlats: returns false for undefined', () => {
    assert.equal(preferFlats(undefined), false);
});

test('preferFlats: returns false for empty string', () => {
    assert.equal(preferFlats(''), false);
});

// ── midiToNote (sharp names) ──────────────────────────────────────────────────

test('midiToNote: A4 (midi 69) is "A" regardless of useFlats', () => {
    assert.equal(midiToNote(69, false), 'A');
    assert.equal(midiToNote(69, true),  'A');
});

test('midiToNote: C4 (midi 60) is "C" regardless of useFlats', () => {
    assert.equal(midiToNote(60, false), 'C');
    assert.equal(midiToNote(60, true),  'C');
});

test('midiToNote: midi 61 is "C#" with sharps', () => {
    assert.equal(midiToNote(61, false), 'C#');
});

test('midiToNote: midi 61 is "Db" with flats', () => {
    assert.equal(midiToNote(61, true), 'Db');
});

test('midiToNote: midi 63 is "D#" with sharps', () => {
    assert.equal(midiToNote(63, false), 'D#');
});

test('midiToNote: midi 63 is "Eb" with flats', () => {
    assert.equal(midiToNote(63, true), 'Eb');
});

test('midiToNote: midi 66 is "F#" with sharps', () => {
    assert.equal(midiToNote(66, false), 'F#');
});

test('midiToNote: midi 66 is "Gb" with flats', () => {
    assert.equal(midiToNote(66, true), 'Gb');
});

test('midiToNote: midi 68 is "G#" with sharps', () => {
    assert.equal(midiToNote(68, false), 'G#');
});

test('midiToNote: midi 68 is "Ab" with flats', () => {
    assert.equal(midiToNote(68, true), 'Ab');
});

test('midiToNote: midi 70 is "A#" with sharps', () => {
    assert.equal(midiToNote(70, false), 'A#');
});

test('midiToNote: midi 70 is "Bb" with flats', () => {
    assert.equal(midiToNote(70, true), 'Bb');
});

test('midiToNote: natural notes are identical regardless of useFlats (E)', () => {
    // E2 = midi 40
    assert.equal(midiToNote(40, false), midiToNote(40, true));
});

test('midiToNote: natural notes are identical regardless of useFlats (B)', () => {
    // B3 = midi 59
    assert.equal(midiToNote(59, false), midiToNote(59, true));
});

test('midiToNote: works with fractional midi (rounds to nearest semitone)', () => {
    // 60.4 rounds to 60 → C
    assert.equal(midiToNote(60.4, false), 'C');
    // 60.6 rounds to 61 → C#
    assert.equal(midiToNote(60.6, false), 'C#');
});

test('midiToNote: handles midi values below C4 (wraps correctly)', () => {
    // midi 48 = C3
    assert.equal(midiToNote(48, false), 'C');
    // midi 47 = B2
    assert.equal(midiToNote(47, false), 'B');
});

// ── freqToMidi / midiToFreq round-trip ───────────────────────────────────────

test('freqToMidi(440) equals 69', () => {
    assert.ok(Math.abs(freqToMidi(440) - 69) < 0.001);
});

test('midiToFreq(69) equals 440 Hz', () => {
    assert.ok(Math.abs(midiToFreq(69) - 440) < 0.01);
});

test('freqToMidi / midiToFreq round-trip within 0.01 Hz', () => {
    for (const freq of [82.41, 110, 146.83, 196, 246.94, 329.63]) {
        const back = midiToFreq(freqToMidi(freq));
        assert.ok(Math.abs(back - freq) < 0.01, `round-trip failed for ${freq} Hz`);
    }
});

// ── offsetsToFreqs ────────────────────────────────────────────────────────────

test('offsetsToFreqs: all-zero 6-string is E Standard open strings', () => {
    const freqs = offsetsToFreqs([0, 0, 0, 0, 0, 0], false);
    // E2 A2 D3 G3 B3 E4 = 82.41 110 146.83 196 246.94 329.63
    const expected = [82.41, 110.00, 146.83, 196.00, 246.94, 329.63];
    freqs.forEach((f, i) => assert.ok(Math.abs(f - expected[i]) < 0.5, `string ${i}: ${f} vs ${expected[i]}`));
});

test('offsetsToFreqs: -1 offset on all strings gives Eb Standard', () => {
    const freqs = offsetsToFreqs([-1, -1, -1, -1, -1, -1], false);
    const eStandard = offsetsToFreqs([0, 0, 0, 0, 0, 0], false);
    // Each string should be one semitone below E Standard
    freqs.forEach((f, i) => {
        const ratio = eStandard[i] / f;
        assert.ok(Math.abs(ratio - Math.pow(2, 1/12)) < 0.01, `string ${i} ratio off`);
    });
});

test('offsetsToFreqs: all-zero 4-string bass is E Standard bass', () => {
    const freqs = offsetsToFreqs([0, 0, 0, 0], true);
    // E1 A1 D2 G2 = 41.20 55.00 73.42 98.00
    const expected = [41.20, 55.00, 73.42, 98.00];
    freqs.forEach((f, i) => assert.ok(Math.abs(f - expected[i]) < 0.5, `string ${i}: ${f} vs ${expected[i]}`));
});

// ── getTuningName ─────────────────────────────────────────────────────────────

test('getTuningName: all zeros → "E Standard"', () => {
    assert.equal(getTuningName([0, 0, 0, 0, 0, 0]), 'E Standard');
});

test('getTuningName: all -1 → "Eb Standard"', () => {
    assert.equal(getTuningName([-1, -1, -1, -1, -1, -1]), 'Eb Standard');
});

test('getTuningName: Drop D pattern → "Drop D"', () => {
    assert.equal(getTuningName([-2, 0, 0, 0, 0, 0]), 'Drop D');
});

test('getTuningName: empty → "Unknown"', () => {
    assert.equal(getTuningName([]), 'Unknown');
});

test('getTuningName: null → "Unknown"', () => {
    assert.equal(getTuningName(null), 'Unknown');
});
