/**
 * The export is the whole payoff, so it gets the tests.
 *
 * Everything else on this page fails loudly — a broken fetch shows an error, a bad
 * decode throws. The export fails *silently*: it produces plausible Tidal that plays
 * the wrong sample, at the wrong length, on an orbit that is already in use, and you
 * find out on stage. So the rules that are easy to get backwards get pinned here.
 */
import { describe, expect, it } from 'vitest'
import { maskOf, stepsOf, toTidal, toStrudel, FAMILY_ORBIT } from '../tidal'
import { loopAtRate, barErrorMs, PHRASE_BARS, CHOP_STEPS, type Voice } from '../engine'

const voice = (o: Partial<Voice> = {}): Voice => ({
  id: 'fred_marea_drums/03_kit_4b', url: '/x.wav', bars: 4, durS: 7.805,
  gain: 1, muted: false,
  cells: Array.from({ length: PHRASE_BARS }, (_, i) => i % 4 === 0),
  ...o,
})

describe('loopAt', () => {
  it('is the ratio the deck plays and the number Tidal is told', () => {
    // 4 bars at 123 bpm is 7.8049 s; a file already that long plays at rate 1
    expect(loopAtRate(7.8049, 4, 123)).toBeCloseTo(1, 3)
    // the same file asked to be 4 bars of 150 bpm has to run faster
    expect(loopAtRate(7.8049, 4, 150)).toBeCloseTo(150 / 123, 3)
  })

  it('never divides by zero on a chop', () => {
    expect(loopAtRate(0.4, 0, 123)).toBe(1)
  })

  it('reports how far off a whole bar a file is, in ms', () => {
    expect(barErrorMs(7.8049, 4, 123)).toBeCloseTo(0, 1)
    expect(barErrorMs(7.9049, 4, 123)).toBeCloseTo(100, 0)
  })
})

describe('mask', () => {
  it('is omitted when every entry is on — an all-t mask is noise in the source', () => {
    expect(maskOf([true, false, false, false, true, false, false, false], 4, 8)).toBeNull()
  })

  it('samples the grid on the loop\'s own stride, not every bar', () => {
    // a 4-bar loop in an 8-bar phrase has TWO entries. Reading all eight cells would
    // emit `mask "t f f f f f f f"` and silence the second entry that is actually on.
    const cells = [true, false, false, false, false, false, false, false]
    expect(maskOf(cells, 4, 8)).toBe('t f')
    expect(maskOf(cells, 1, 8)).toBe('t f f f f f f f')
  })

  it('renders a chop row as a structure string of the right length', () => {
    const c = Array.from({ length: CHOP_STEPS }, (_, i) => i % 4 === 0)
    expect(stepsOf(c).split(' ')).toHaveLength(CHOP_STEPS)
    expect(stepsOf(c)).toBe('x ~ ~ ~ x ~ ~ ~ x ~ ~ ~ x ~ ~ ~')
  })
})

describe('toTidal', () => {
  const row = (o: Partial<Voice> = {}, family = 'drums', n = 3) =>
    ({ voice: voice(o), kit: 'fred_marea_drums', n, family })

  it('emits the index and the bar count together, because both must be right', () => {
    const out = toTidal([row()], 123)
    expect(out).toContain('loopAt 4 $ s "fred_marea_drums" # n 3')
    expect(out).toContain('setcps (0.5125)')      // 123 bpm / 60 / 4
  })

  it('converts the linear fader to Tidal\'s quartic gain', () => {
    // amp = gain^4, so a fader at half amplitude is gain 0.5^(1/4) = 0.84, not 0.5.
    // Copying the fader straight across would be a 12 dB error.
    expect(toTidal([row({ gain: 0.5 })], 123)).toContain('# gain 0.84')
    // and unity is left off entirely rather than written as `# gain 1.00`
    expect(toTidal([row({ gain: 1 })], 123)).not.toContain('# gain')
  })

  it('puts each family on its own orbit and never doubles up', () => {
    const rows = [row({ id: 'a/1' }, 'drums', 0), row({ id: 'b/2' }, 'drums', 1)]
    const lines = toTidal(rows, 123).split('\n').filter((l) => l.startsWith('d'))
    const orbits = lines.map((l) => l.slice(0, 2))
    expect(new Set(orbits).size).toBe(2)          // two voices, two orbits
    expect(orbits[0]).toBe(`d${FAMILY_ORBIT.drums}`)
  })

  it('skips muted rows and always ends on hush', () => {
    const out = toTidal([row({ muted: true })], 123)
    expect(out).not.toContain('loopAt')
    expect(out.trimEnd().endsWith('hush')).toBe(true)
  })

  it('emits a chop as a structure, not as a loopAt', () => {
    const out = toTidal([row({ bars: 0, cells: [true, false, false, false] })], 123)
    expect(out).toContain('# n 3')
    expect(out).not.toContain('loopAt')
  })
})

describe('toStrudel', () => {
  it('is silence when nothing is armed, never an empty stack', () => {
    expect(toStrudel([], 123)).toBe('silence')
    expect(toStrudel([{ voice: voice({ muted: true }), kit: 'k', n: 0, family: 'drums' }], 123))
      .toBe('silence')
  })

  it('carries the same kit, index and bar count as the Tidal form', () => {
    const out = toStrudel([{ voice: voice(), kit: 'fred_marea_drums', n: 3, family: 'drums' }], 123)
    expect(out).toContain('s("fred_marea_drums").n(3).loopAt(4)')
  })
})
