// @vitest-environment happy-dom
import { beforeEach, describe, expect, it } from 'vitest';
import { BAND_RANGE, createEchoRadio, resolveRadio, type RadioControl } from './radio';
import { createTuner } from './tuner';

const q = (s: string) => document.querySelector(s) as HTMLElement;
const bandBtn = (b: string) =>
  Array.from(document.querySelectorAll('.fk-tuner-band')).find((x) => x.textContent === b) as HTMLElement;

function mount() {
  document.body.innerHTML = '';
  const tuner = createTuner();
  document.body.append(tuner.el);
  return tuner;
}

describe('radio control resolution', () => {
  beforeEach(() => {
    delete (globalThis as any).__fktrySpeaker;
    delete (globalThis as any).__fktryAudioModule;
  });

  it('falls back to a silent echo stand-in when THE SPEAKER has published nothing', () => {
    const { live } = resolveRadio();
    expect(live).toBe(false);
  });

  it('uses the real control the moment SCREEN publishes one', () => {
    const fake = createEchoRadio();
    (globalThis as any).__fktrySpeaker = { radio: fake };
    const { control, live } = resolveRadio();
    expect(live).toBe(true);
    expect(control).toBe(fake);
    delete (globalThis as any).__fktrySpeaker;
  });

  it('ignores a published object that is not a radio control', () => {
    (globalThis as any).__fktrySpeaker = { radio: { nope: true } };
    expect(resolveRadio().live).toBe(false);
    delete (globalThis as any).__fktrySpeaker;
  });
});

describe('echo radio', () => {
  it('notifies subscribers immediately and on every change', () => {
    const r = createEchoRadio();
    const seen: number[] = [];
    r.subscribe((s) => seen.push(s.freqMHz));
    r.setFrequency(101.1);
    expect(seen).toHaveLength(2); // initial + change
    expect(seen[1]).toBe(101.1);
  });

  it('snaps to the band floor when the band changes', () => {
    const r = createEchoRadio();
    r.setBand('AM');
    expect(r.getState().band).toBe('AM');
    expect(r.getState().freqMHz).toBe(BAND_RANGE.AM.min);
  });

  it('clamps volume to 0..1', () => {
    const r = createEchoRadio();
    r.volume(5);
    expect(r.getState().volume).toBe(1);
    r.volume(-2);
    expect(r.getState().volume).toBe(0);
  });

  it('unsubscribes cleanly', () => {
    const r = createEchoRadio();
    let n = 0;
    const off = r.subscribe(() => n++);
    off();
    r.setFrequency(99);
    expect(n).toBe(1); // only the initial call
  });
});

describe('the tuner dock', () => {
  let tuner: ReturnType<typeof createTuner>;
  beforeEach(() => {
    delete (globalThis as any).__fktrySpeaker;
    tuner = mount();
  });

  it('stays hidden until the fossil is found', () => {
    expect(tuner.isRevealed()).toBe(false);
    expect(tuner.el.classList.contains('is-revealed')).toBe(false);
  });

  it('reveals on discovery, and revealing twice is harmless', () => {
    tuner.reveal();
    tuner.reveal();
    expect(tuner.isRevealed()).toBe(true);
    expect(tuner.el.classList.contains('is-revealed')).toBe(true);
  });

  it('ignores its keys while hidden, so they never shadow the pre-discovery game', () => {
    expect(tuner.onKey('arrowright')).toBe(false);
    expect(tuner.onKey('m')).toBe(false);
  });

  it('tunes with the arrow keys once revealed', () => {
    tuner.reveal();
    const before = q('.fk-tuner-readout').textContent!;
    expect(tuner.onKey('arrowright')).toBe(true);
    expect(q('.fk-tuner-readout').textContent).not.toBe(before);
  });

  it('does not tune past the end of the band', () => {
    tuner.reveal();
    for (let i = 0; i < 400; i++) tuner.onKey('arrowleft'); // hammer the bottom stop
    const shown = q('.fk-tuner-readout').textContent!;
    expect(shown).toContain(BAND_RANGE.FM.min.toFixed(1));
  });

  it('cycles bands with B and reflects it in the readout and the active button', () => {
    tuner.reveal();
    tuner.onKey('b');
    expect(q('.fk-tuner-readout').textContent).toContain('SW');
    expect(bandBtn('SW').classList.contains('is-active')).toBe(true);
    expect(bandBtn('FM').classList.contains('is-active')).toBe(false);
  });

  it('switches band by clicking a band button', () => {
    tuner.reveal();
    bandBtn('AM').dispatchEvent(new MouseEvent('click', { bubbles: true }));
    expect(q('.fk-tuner-readout').textContent).toContain('AM');
    expect(bandBtn('AM').classList.contains('is-active')).toBe(true);
  });

  it('mutes and unmutes with M, and says so', () => {
    tuner.reveal();
    tuner.onKey('m');
    expect(q('.fk-tuner-mute').textContent).toBe('MUTED');
    expect(tuner.el.classList.contains('is-muted')).toBe(true);
    tuner.onKey('m');
    expect(q('.fk-tuner-mute').textContent).toBe('MUTE');
    expect(tuner.el.classList.contains('is-muted')).toBe(false);
  });

  it('mutes by clicking the mute button', () => {
    tuner.reveal();
    q('.fk-tuner-mute').dispatchEvent(new MouseEvent('click', { bubbles: true }));
    expect(q('.fk-tuner-mute').textContent).toBe('MUTED');
  });

  it('reads out static when no station locks, in the house voice', () => {
    tuner.reveal();
    expect(q('.fk-tuner-readout').textContent).toContain('STATIC');
    expect(q('.fk-tuner-readout').classList.contains('is-locked')).toBe(false);
  });

  it('draws no station ticks before the engine attaches (empty pre-gesture)', () => {
    tuner.reveal();
    // The echo stand-in knows no stations, exactly like the adapter before first gesture.
    expect(document.querySelectorAll('.fk-tuner-tick')).toHaveLength(0);
  });

  it('draws a tick per station on the current band, positioned by frequency', () => {
    const control: RadioControl = {
      ...createEchoRadio(),
      subscribe(cb) {
        cb({ freqMHz: 88.0, band: 'FM', volume: 0.7, muted: false, station: null });
        return () => {};
      },
      listStations: () => [
        { id: 'a', name: 'A', band: 'FM', freqMHz: 88.0 },   // FM floor → left edge
        { id: 'b', name: 'B', band: 'FM', freqMHz: 108.0 },  // FM ceiling → right edge
        { id: 'c', name: 'C', band: 'AM', freqMHz: 1.0 },    // other band → not drawn
      ],
    };
    (globalThis as any).__fktrySpeaker = { radio: control };
    const t = mount();
    t.reveal();
    const ticks = Array.from(document.querySelectorAll('.fk-tuner-tick')) as HTMLElement[];
    expect(ticks).toHaveLength(2); // only the two FM stations
    expect(ticks[0].style.left).toBe('0%');
    expect(ticks[1].style.left).toBe('100%');
    // Ticks are anonymous — they mark THAT, not WHAT (the hunt stays).
    expect(ticks[0].textContent).toBe('');
    delete (globalThis as any).__fktrySpeaker;
  });

  it('reads SIGNAL LOCKED and lights up when the engine reports a station', () => {
    // What it looks like once THE SPEAKER is driving it for real.
    const withStation: RadioControl = {
      ...createEchoRadio(),
      subscribe(cb) {
        cb({ freqMHz: 88.1, band: 'FM', volume: 0.7, muted: false, station: 'FKTRY' });
        return () => {};
      },
    };
    (globalThis as any).__fktrySpeaker = { radio: withStation };
    const t = mount();
    t.reveal();
    expect(q('.fk-tuner-readout').textContent).toBe('88.1 FKTRY FM — SIGNAL LOCKED');
    expect(q('.fk-tuner-readout').classList.contains('is-locked')).toBe(true);
    expect(t.isLive()).toBe(true);
    delete (globalThis as any).__fktrySpeaker;
  });
});
