import assert from 'node:assert/strict'
import { createRequire } from 'node:module'
import test from 'node:test'

const require = createRequire(import.meta.url)
const nativeAddon = require('../native/build/Release/visualizer_dsp.node')
const { spectrum } = nativeAddon

const SAMPLE_RATE = 48000
const SILENCE_DB = -120

function createTone(frequencyHz, length, amplitude = 1) {
  const samples = new Float32Array(length)
  for (let index = 0; index < length; index += 1) {
    samples[index] = Math.sin((2 * Math.PI * frequencyHz * index) / SAMPLE_RATE) * amplitude
  }
  return samples
}

function configure(fftSize) {
  spectrum.setFFTSize(fftSize)
  spectrum.setSampleRate(SAMPLE_RATE)
  spectrum.setSmoothing(0)
  spectrum.reset()
}

function assertAlmostEqual(actual, expected, tolerance, message) {
  assert.ok(
    Math.abs(actual - expected) <= tolerance,
    `${message}: expected ${expected} +/- ${tolerance}, got ${actual}`,
  )
}

function interpolatePeakDb(magnitudes) {
  let peakBin = 1
  for (let index = 2; index < magnitudes.length - 1; index += 1) {
    if (magnitudes[index] > magnitudes[peakBin]) peakBin = index
  }

  const y1 = magnitudes[peakBin - 1]
  const y2 = magnitudes[peakBin]
  const y3 = magnitudes[peakBin + 1]
  const denominator = y1 - (2 * y2) + y3
  if (Math.abs(denominator) <= 1e-9) return y2
  const offset = Math.max(-0.5, Math.min(0.5, 0.5 * (y1 - y3) / denominator))
  return y2 - (0.25 * (y1 - y3) * offset)
}

test('native capture exports preserve the renderer-facing API shape', () => {
  for (const exportName of ['macosCapture', 'windowsCapture', 'linuxCapture']) {
    const capture = nativeAddon[exportName]
    assert.ok(capture, `${exportName} should be exported`)
    for (const method of ['getSupport', 'listOutputDevices', 'start', 'stop', 'drain', 'nowMilliseconds']) {
      assert.equal(typeof capture[method], 'function', `${exportName}.${method} should be a function`)
    }
  }

  const activeExport = process.platform === 'darwin'
    ? 'macosCapture'
    : process.platform === 'win32'
      ? 'windowsCapture'
      : 'linuxCapture'
  const support = nativeAddon[activeExport].getSupport()
  assert.equal(typeof support.available, 'boolean')
  assert.ok(support.reason === null || typeof support.reason === 'string')

  for (const exportName of ['macosCapture', 'windowsCapture', 'linuxCapture']) {
    if (exportName === activeExport) continue
    const capture = nativeAddon[exportName]
    assert.deepEqual(capture.listOutputDevices(), [])
    assert.equal(capture.getSupport().available, false)
    assert.deepEqual(capture.drain(), { chunks: [], overwriteCount: 0, queueDepth: 0 })
    assert.throws(() => capture.start(), /unavailable on the current platform/)
  }

  assert.ok(nativeAddon.windowsMedia)
  for (const method of ['getSupport', 'getSpotifyPlaybackState', 'sendSpotifyControl']) {
    assert.equal(typeof nativeAddon.windowsMedia[method], 'function')
  }
})

test('spectrum channel-max dBFS is calibrated for bin-centered amplitudes', () => {
  const fftSize = 2048
  const bin = 42
  const frequencyHz = bin * SAMPLE_RATE / fftSize

  for (const amplitude of [1, 0.5, 0.1]) {
    configure(fftSize)
    spectrum.pushSamples(createTone(frequencyHz, fftSize, amplitude))
    const expectedDbfs = 20 * Math.log10(amplitude)
    const filledMagnitudes = new Float32Array(fftSize / 2)
    assert.equal(spectrum.fillChannelMaxMagnitudes(filledMagnitudes), fftSize / 2)
    assertAlmostEqual(
      filledMagnitudes[bin],
      expectedDbfs,
      0.05,
      `amplitude ${amplitude}`,
    )
  }
})

test('spectrum off-bin dBFS interpolation stays within 0.3 dB across FFT sizes', () => {
  for (const fftSize of [1024, 2048, 4096, 8192, 16384]) {
    configure(fftSize)
    spectrum.pushSamples(createTone(440, fftSize))
    assertAlmostEqual(
      interpolatePeakDb(spectrum.getChannelMaxMagnitudes()),
      0,
      0.3,
      `FFT size ${fftSize}`,
    )
  }
})

test('spectrum preserves Mid/Side curves while channel-max follows the louder L/R channel', () => {
  const fftSize = 2048
  const bin = 42
  const frequencyHz = bin * SAMPLE_RATE / fftSize
  const fullScale = createTone(frequencyHz, fftSize)
  const halfScale = createTone(frequencyHz, fftSize, 0.5)
  const silence = new Float32Array(fftSize)
  const inverted = Float32Array.from(fullScale, (sample) => -sample)

  configure(fftSize)
  spectrum.pushStereoSamples(fullScale, silence)
  assertAlmostEqual(spectrum.getMagnitudes()[bin], -6.0206, 0.05, 'left-only Mid level')
  assertAlmostEqual(spectrum.getSideMagnitudes()[bin], -6.0206, 0.05, 'left-only Side level')
  assertAlmostEqual(spectrum.getChannelMaxMagnitudes()[bin], 0, 0.05, 'left-only channel max')

  configure(fftSize)
  spectrum.pushStereoSamples(silence, fullScale)
  assertAlmostEqual(spectrum.getChannelMaxMagnitudes()[bin], 0, 0.05, 'right-only channel max')

  configure(fftSize)
  spectrum.pushStereoSamples(halfScale, fullScale)
  assertAlmostEqual(spectrum.getChannelMaxMagnitudes()[bin], 0, 0.05, 'unequal-channel max')

  configure(fftSize)
  spectrum.pushStereoSamples(fullScale, inverted)
  assert.equal(spectrum.getMagnitudes()[bin], SILENCE_DB)
  assertAlmostEqual(spectrum.getSideMagnitudes()[bin], 0, 0.05, 'anti-phase Side level')
  assertAlmostEqual(spectrum.getChannelMaxMagnitudes()[bin], 0, 0.05, 'anti-phase channel max')
})
