/**
 * Unit tests for the Push-to-Talk service (src/lib/ptt.ts).
 *
 * Covers:
 *  - vkName: known keys, A-Z, 0-9, Numpad, unknown (hex fallback)
 *  - initPtt: no-op when pttVk is 0; invokes ptt_set_key + ptt_start when set
 *  - stopPtt: calls invoke("ptt_stop"); no-op when not listening
 *  - updatePttKey: saves pref, calls ptt_set_key; calls stopPtt when vk === 0
 *  - captureKeyPress: calls invoke("ptt_listen_for_key")
 */
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

// ---------------------------------------------------------------------------
// Shared mock state
// ---------------------------------------------------------------------------

const mockInvoke = vi.fn();
const mockListen = vi.fn();

// Prefs storage
const testPrefs = new Map<string, unknown>();

// voiceStore state
let mockCurrentChannelId: number | null = null;
let mockLocalMuted = false;
let mockLocalDeafened = false;
let mockPttGated = false;
let mockPttPollingLive = false;
const mockSetPttGated = vi.fn();
const mockSetPttPollingLive = vi.fn((live: boolean) => {
  mockPttPollingLive = live;
});

/** Captures the listener passed to voiceStore.subscribe() so tests can fire
 *  a simulated store notification (real createStore() batches these via
 *  queueMicrotask; the mock fires only when a test invokes it explicitly). */
let capturedStoreListener: ((state: { localMuted: boolean }) => void) | null = null;
const mockSubscribeStore = vi.fn((listener: (state: { localMuted: boolean }) => void) => {
  capturedStoreListener = listener;
  return () => {
    capturedStoreListener = null;
  };
});

// ---------------------------------------------------------------------------
// Module mocks (must be declared before importing the module under test)
// ---------------------------------------------------------------------------

vi.mock("@tauri-apps/api/core", () => ({
  invoke: (...args: unknown[]) => mockInvoke(...args),
}));

vi.mock("@tauri-apps/api/event", () => ({
  listen: (...args: unknown[]) => mockListen(...args),
}));

vi.mock("@components/settings/helpers", () => ({
  loadPref: (key: string, fallback: unknown) => testPrefs.get(key) ?? fallback,
  savePref: (key: string, value: unknown) => {
    testPrefs.set(key, value);
  },
}));

vi.mock("@stores/voice.store", () => ({
  voiceStore: {
    getState: () => ({
      currentChannelId: mockCurrentChannelId,
      localMuted: mockLocalMuted,
      localDeafened: mockLocalDeafened,
      pttGated: mockPttGated,
    }),
    subscribe: (listener: (state: { localMuted: boolean }) => void) => mockSubscribeStore(listener),
  },
  setPttGated: (...args: unknown[]) => mockSetPttGated(...args),
  setPttPollingLive: (live: boolean) => mockSetPttPollingLive(live),
  isPttPollingLive: () => mockPttPollingLive,
}));

vi.mock("@lib/logger", () => ({
  createLogger: () => ({
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
    debug: vi.fn(),
  }),
}));

// setMuted is called internally by the ptt-state listener — mock to isolate
vi.mock("../../src/lib/livekitSession", () => ({
  setMuted: vi.fn(),
}));

// ---------------------------------------------------------------------------
// Import module under test (AFTER mocks)
// ---------------------------------------------------------------------------

import { vkName, initPtt, stopPtt, updatePttKey, captureKeyPress } from "../../src/lib/ptt";

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/** Reset all mock state between tests. */
function resetAll(): void {
  testPrefs.clear();
  mockCurrentChannelId = null;
  mockLocalMuted = false;
  mockLocalDeafened = false;
  mockPttGated = false;
  mockPttPollingLive = false;
  mockSetPttGated.mockReset();
  mockSetPttPollingLive.mockReset();
  mockSetPttPollingLive.mockImplementation((live: boolean) => {
    mockPttPollingLive = live;
  });
  mockInvoke.mockReset();
  mockListen.mockReset();
  mockSubscribeStore.mockReset();
  mockSubscribeStore.mockImplementation((listener: (state: { localMuted: boolean }) => void) => {
    capturedStoreListener = listener;
    return () => {
      capturedStoreListener = null;
    };
  });
  capturedStoreListener = null;
  // Default: invoke resolves with undefined; listen resolves with a no-op unlistener
  mockInvoke.mockResolvedValue(undefined);
  mockListen.mockResolvedValue(() => {});
}

// ---------------------------------------------------------------------------
// Tests: vkName
// ---------------------------------------------------------------------------

describe("vkName", () => {
  describe("well-known named keys", () => {
    it("returns 'Mouse 4' for 0x05", () => {
      expect(vkName(0x05)).toBe("Mouse 4");
    });

    it("returns 'Mouse 5' for 0x06", () => {
      expect(vkName(0x06)).toBe("Mouse 5");
    });

    it("returns 'Mouse Left' for 0x01", () => {
      expect(vkName(0x01)).toBe("Mouse Left");
    });

    it("returns 'Mouse Right' for 0x02", () => {
      expect(vkName(0x02)).toBe("Mouse Right");
    });

    it("returns 'Mouse Middle' for 0x04", () => {
      expect(vkName(0x04)).toBe("Mouse Middle");
    });

    it("returns 'Space' for 0x20", () => {
      expect(vkName(0x20)).toBe("Space");
    });

    it("returns 'F1' for 0x70", () => {
      expect(vkName(0x70)).toBe("F1");
    });

    it("returns 'F12' for 0x7B", () => {
      expect(vkName(0x7b)).toBe("F12");
    });

    it("returns 'Enter' for 0x0D", () => {
      expect(vkName(0x0d)).toBe("Enter");
    });

    it("returns 'Escape' for 0x1B", () => {
      expect(vkName(0x1b)).toBe("Escape");
    });

    it("returns 'Backspace' for 0x08", () => {
      expect(vkName(0x08)).toBe("Backspace");
    });

    it("returns 'Tab' for 0x09", () => {
      expect(vkName(0x09)).toBe("Tab");
    });

    it("returns 'Delete' for 0x2E", () => {
      expect(vkName(0x2e)).toBe("Delete");
    });

    it("returns 'Insert' for 0x2D", () => {
      expect(vkName(0x2d)).toBe("Insert");
    });

    it("returns 'Arrow Left' for 0x25", () => {
      expect(vkName(0x25)).toBe("Arrow Left");
    });

    it("returns 'Arrow Right' for 0x27", () => {
      expect(vkName(0x27)).toBe("Arrow Right");
    });

    it("returns 'Arrow Up' for 0x26", () => {
      expect(vkName(0x26)).toBe("Arrow Up");
    });

    it("returns 'Arrow Down' for 0x28", () => {
      expect(vkName(0x28)).toBe("Arrow Down");
    });

    it("returns 'Page Up' for 0x21", () => {
      expect(vkName(0x21)).toBe("Page Up");
    });

    it("returns 'Page Down' for 0x22", () => {
      expect(vkName(0x22)).toBe("Page Down");
    });

    it("returns 'Home' for 0x24", () => {
      expect(vkName(0x24)).toBe("Home");
    });

    it("returns 'End' for 0x23", () => {
      expect(vkName(0x23)).toBe("End");
    });
  });

  describe("digit keys 0-9 (0x30-0x39)", () => {
    it("returns '0' for 0x30", () => {
      expect(vkName(0x30)).toBe("0");
    });

    it("returns '9' for 0x39", () => {
      expect(vkName(0x39)).toBe("9");
    });

    it("returns '5' for 0x35", () => {
      expect(vkName(0x35)).toBe("5");
    });
  });

  describe("letter keys A-Z (0x41-0x5A)", () => {
    it("returns 'A' for 0x41", () => {
      expect(vkName(0x41)).toBe("A");
    });

    it("returns 'Z' for 0x5A", () => {
      expect(vkName(0x5a)).toBe("Z");
    });

    it("returns 'M' for 0x4D", () => {
      expect(vkName(0x4d)).toBe("M");
    });
  });

  describe("Numpad keys (0x60-0x69)", () => {
    it("returns 'Numpad 0' for 0x60", () => {
      expect(vkName(0x60)).toBe("Numpad 0");
    });

    it("returns 'Numpad 9' for 0x69", () => {
      expect(vkName(0x69)).toBe("Numpad 9");
    });

    it("returns 'Numpad 5' for 0x65", () => {
      expect(vkName(0x65)).toBe("Numpad 5");
    });
  });

  describe("unknown keys — hex fallback", () => {
    it("returns hex string for an unrecognised VK code", () => {
      // 0xFF is not in the map and not in any named range
      expect(vkName(0xff)).toBe("Key 0xFF");
    });

    it("returns uppercase hex for 0xAB", () => {
      expect(vkName(0xab)).toBe("Key 0xAB");
    });

    it("returns 'Key 0x0' for vk code 0", () => {
      // 0 is unrecognised — not in map, not in any character range
      expect(vkName(0x00)).toBe("Key 0x0");
    });
  });
});

// ---------------------------------------------------------------------------
// Tests: initPtt
// ---------------------------------------------------------------------------

describe("initPtt", () => {
  beforeEach(resetAll);

  it("does nothing when pttVk pref is 0 (default)", async () => {
    // pttVk defaults to 0
    await initPtt();

    expect(mockInvoke).not.toHaveBeenCalled();
    expect(mockListen).not.toHaveBeenCalled();
  });

  it("does nothing when pttVk pref is explicitly saved as 0", async () => {
    testPrefs.set("pttVk", 0);

    await initPtt();

    expect(mockInvoke).not.toHaveBeenCalled();
    expect(mockListen).not.toHaveBeenCalled();
  });

  it("calls invoke('ptt_set_key') with the stored vk code when key is non-zero", async () => {
    testPrefs.set("pttVk", 0x20); // Space

    await initPtt();

    expect(mockInvoke).toHaveBeenCalledWith("ptt_set_key", { vkCode: 0x20 });
  });

  it("calls invoke('ptt_start') when key is non-zero", async () => {
    testPrefs.set("pttVk", 0x20);

    await initPtt();

    expect(mockInvoke).toHaveBeenCalledWith("ptt_start");
  });

  it("calls ptt_set_key before ptt_start", async () => {
    testPrefs.set("pttVk", 0x41); // A

    await initPtt();

    const calls = mockInvoke.mock.calls.map((c) => c[0]);
    const setKeyIdx = calls.indexOf("ptt_set_key");
    const startIdx = calls.indexOf("ptt_start");
    expect(setKeyIdx).toBeGreaterThanOrEqual(0);
    expect(startIdx).toBeGreaterThanOrEqual(0);
    expect(setKeyIdx).toBeLessThan(startIdx);
  });

  // v007: livekitSession mutes the mic at join when PTT is armed, and only a
  // real ptt-state event can lift that mute. ptt_start spawns its thread on
  // every platform, so the frontend must gate on the backend's capability
  // answer instead — otherwise macOS (is_key_down stub) and pure-Wayland Linux
  // (no reachable display) join muted with no way to ever unmute.
  it("reports the backend's polling capability so join-time muting is safe", async () => {
    testPrefs.set("pttVk", 0x20);
    mockInvoke.mockImplementation((cmd: string) =>
      Promise.resolve(cmd === "ptt_polling_supported" ? true : undefined),
    );

    await initPtt();

    expect(mockInvoke).toHaveBeenCalledWith("ptt_polling_supported");
    expect(mockSetPttPollingLive).toHaveBeenCalledWith(true);
  });

  it("reports polling as NOT live when the backend cannot observe key state", async () => {
    testPrefs.set("pttVk", 0x20);
    mockInvoke.mockImplementation((cmd: string) =>
      Promise.resolve(cmd === "ptt_polling_supported" ? false : undefined),
    );

    await initPtt();

    expect(mockSetPttPollingLive).toHaveBeenCalledWith(false);
    expect(mockSetPttPollingLive).not.toHaveBeenCalledWith(true);
  });

  it("reports polling as NOT live when a backend command rejects", async () => {
    testPrefs.set("pttVk", 0x20);
    mockInvoke.mockRejectedValue(new Error("not in Tauri"));

    await initPtt();

    expect(mockSetPttPollingLive).toHaveBeenCalledWith(false);
    expect(mockSetPttPollingLive).not.toHaveBeenCalledWith(true);
  });

  it("calls listen for 'ptt-state' events when key is non-zero", async () => {
    testPrefs.set("pttVk", 0x70); // F1

    await initPtt();

    expect(mockListen).toHaveBeenCalledWith("ptt-state", expect.any(Function));
  });

  it("does not throw when Tauri is unavailable (simulated by invoke rejecting)", async () => {
    testPrefs.set("pttVk", 0x20);
    mockInvoke.mockRejectedValue(new Error("not in Tauri"));

    await expect(initPtt()).resolves.toBeUndefined();
  });
});

// ---------------------------------------------------------------------------
// Tests: stopPtt
// ---------------------------------------------------------------------------

describe("stopPtt", () => {
  beforeEach(async () => {
    resetAll();
    // Drain any lingering listening state left by earlier test groups.
    // stopPtt with listening===true would call invoke — clear it silently.
    await stopPtt();
    mockInvoke.mockClear();
  });

  it("does not call invoke when PTT was never started (not listening)", async () => {
    // After beforeEach drain, listening is false — stopPtt must be a no-op.
    await stopPtt();

    expect(mockInvoke).not.toHaveBeenCalled();
  });

  it("calls invoke('ptt_stop') after PTT has been started", async () => {
    // Start PTT first so the listening flag is set
    testPrefs.set("pttVk", 0x20);
    await initPtt();
    mockInvoke.mockClear();

    await stopPtt();

    expect(mockInvoke).toHaveBeenCalledWith("ptt_stop");
  });

  it("does not throw when invoke('ptt_stop') rejects", async () => {
    testPrefs.set("pttVk", 0x20);
    await initPtt();
    mockInvoke.mockRejectedValue(new Error("ptt_stop failed"));

    await expect(stopPtt()).resolves.toBeUndefined();
  });

  it("is idempotent — second stopPtt does not call invoke again", async () => {
    testPrefs.set("pttVk", 0x20);
    await initPtt();

    await stopPtt();
    mockInvoke.mockClear();

    await stopPtt();

    expect(mockInvoke).not.toHaveBeenCalled();
  });
});

// ---------------------------------------------------------------------------
// Tests: updatePttKey
// ---------------------------------------------------------------------------

describe("updatePttKey", () => {
  beforeEach(resetAll);

  it("saves the new vk code to prefs", async () => {
    await updatePttKey(0x41);

    expect(testPrefs.get("pttVk")).toBe(0x41);
  });

  it("calls invoke('ptt_set_key') with the new vk code", async () => {
    await updatePttKey(0x41);

    expect(mockInvoke).toHaveBeenCalledWith("ptt_set_key", { vkCode: 0x41 });
  });

  it("saves 0 to prefs when called with 0 (disable PTT)", async () => {
    // Start listening first
    testPrefs.set("pttVk", 0x20);
    await initPtt();

    await updatePttKey(0);

    expect(testPrefs.get("pttVk")).toBe(0);
  });

  it("calls invoke('ptt_stop') via stopPtt when vk is 0 and was listening", async () => {
    // Establish listening state
    testPrefs.set("pttVk", 0x20);
    await initPtt();
    mockInvoke.mockClear();

    await updatePttKey(0);

    expect(mockInvoke).toHaveBeenCalledWith("ptt_stop");
  });

  it("does not call ptt_stop when vk is 0 but was never listening", async () => {
    // Never called initPtt, so listening === false
    await updatePttKey(0);

    // ptt_set_key should be called (updatePttKey always calls it), but not ptt_stop
    const stopCalls = mockInvoke.mock.calls.filter((c) => c[0] === "ptt_stop");
    expect(stopCalls).toHaveLength(0);
  });

  it("does not throw when invoke rejects", async () => {
    mockInvoke.mockRejectedValue(new Error("invoke failed"));

    await expect(updatePttKey(0x20)).resolves.toBeUndefined();
  });

  it("calls initPtt (triggering ptt_start) when setting a key while not yet listening", async () => {
    // listening is false because initPtt was never called
    await updatePttKey(0x41);

    // updatePttKey calls initPtt internally when !listening && vk !== 0,
    // which in turn calls ptt_set_key (again) and ptt_start
    const startCalls = mockInvoke.mock.calls.filter((c) => c[0] === "ptt_start");
    expect(startCalls.length).toBeGreaterThanOrEqual(1);
  });
});

// ---------------------------------------------------------------------------
// Tests: updatePttKey gates an already-hot mic when bound mid-call (OC-0162)
// ---------------------------------------------------------------------------

describe("updatePttKey gates the mic when binding a key mid-call (OC-0162)", () => {
  beforeEach(async () => {
    resetAll();
    // The module-level `listening` flag is not reset by resetAll() (it lives
    // in ptt.ts, not in the mocks) and earlier describe blocks may leave it
    // true — drain it so each test here starts from the same "no key bound
    // yet" state the finding's repro assumes.
    await stopPtt();
    mockInvoke.mockClear();
  });

  it("gates and mutes the mic when a key is bound while already in a voice call", async () => {
    const { setMuted } = await import("../../src/lib/livekitSession");
    const mockSetMuted = vi.mocked(setMuted);
    mockSetMuted.mockClear();

    // Already in a voice call, joined with no PTT key bound — the mic was
    // published ungated (pttArmed was false at join time).
    mockCurrentChannelId = 7;
    mockPttGated = false;
    mockLocalMuted = false;
    mockLocalDeafened = false;
    mockInvoke.mockImplementation((cmd: string) =>
      Promise.resolve(cmd === "ptt_polling_supported" ? true : undefined),
    );

    // The user now binds a PTT key from Settings -> Keybinds.
    await updatePttKey(0x20);

    // Without the fix, updatePttKey only starts the poller (initPtt) and
    // never applies the gate — the idle key produces no ptt-state transition
    // (see src-tauri/src/ptt.rs ptt_transition), so the mic stays hot forever
    // until the user's first physical press+release.
    expect(mockSetPttGated).toHaveBeenCalledWith(true);
    await vi.waitFor(() => {
      expect(mockSetMuted).toHaveBeenCalledWith(true);
    });
  });

  it("does not gate the mic when binding a key while not in a voice call", async () => {
    const { setMuted } = await import("../../src/lib/livekitSession");
    const mockSetMuted = vi.mocked(setMuted);
    mockSetMuted.mockClear();

    mockCurrentChannelId = null; // not in a call
    mockPttGated = false;
    mockInvoke.mockImplementation((cmd: string) =>
      Promise.resolve(cmd === "ptt_polling_supported" ? true : undefined),
    );

    await updatePttKey(0x20);

    expect(mockSetPttGated).not.toHaveBeenCalledWith(true);
    await new Promise((r) => setTimeout(r, 0));
    expect(mockSetMuted).not.toHaveBeenCalledWith(true);
  });

  it("does not gate the mic when the backend cannot actually observe key state", async () => {
    const { setMuted } = await import("../../src/lib/livekitSession");
    const mockSetMuted = vi.mocked(setMuted);
    mockSetMuted.mockClear();

    mockCurrentChannelId = 7; // in a call
    mockPttGated = false;
    // ptt_polling_supported === false: macOS is_key_down stub / Wayland — no
    // ptt-state event can ever arrive, so gating here would strand the mic
    // muted forever with no press able to lift it.
    mockInvoke.mockImplementation((cmd: string) =>
      Promise.resolve(cmd === "ptt_polling_supported" ? false : undefined),
    );

    await updatePttKey(0x20);

    expect(mockSetPttGated).not.toHaveBeenCalledWith(true);
    await new Promise((r) => setTimeout(r, 0));
    expect(mockSetMuted).not.toHaveBeenCalledWith(true);
  });

  it("does not re-gate when the mic is already PTT-gated", async () => {
    const { setMuted } = await import("../../src/lib/livekitSession");
    const mockSetMuted = vi.mocked(setMuted);
    mockSetMuted.mockClear();

    mockCurrentChannelId = 7;
    mockPttGated = true; // already gated (e.g. join-time gate already armed)
    mockInvoke.mockImplementation((cmd: string) =>
      Promise.resolve(cmd === "ptt_polling_supported" ? true : undefined),
    );

    await updatePttKey(0x20);
    await new Promise((r) => setTimeout(r, 0));

    expect(mockSetPttGated).not.toHaveBeenCalledWith(true);
    expect(mockSetMuted).not.toHaveBeenCalledWith(true);
  });
});

// ---------------------------------------------------------------------------
// Tests: captureKeyPress
// ---------------------------------------------------------------------------

describe("captureKeyPress", () => {
  beforeEach(resetAll);

  it("calls invoke('ptt_listen_for_key') and returns the result", async () => {
    mockInvoke.mockResolvedValue(0x41);

    const result = await captureKeyPress();

    expect(mockInvoke).toHaveBeenCalledWith("ptt_listen_for_key");
    expect(result).toBe(0x41);
  });

  it("propagates the vk code returned by Tauri", async () => {
    mockInvoke.mockResolvedValue(0x20);

    const result = await captureKeyPress();

    expect(result).toBe(0x20);
  });

  it("propagates rejection from invoke", async () => {
    mockInvoke.mockRejectedValue(new Error("listener error"));

    await expect(captureKeyPress()).rejects.toThrow("listener error");
  });
});

// ---------------------------------------------------------------------------
// Tests: ptt-state event handler (integration via initPtt + listen callback)
// ---------------------------------------------------------------------------

describe("ptt-state event listener", () => {
  beforeEach(resetAll);

  afterEach(() => {
    vi.resetModules();
  });

  it("calls setMuted(false) when PTT is pressed (payload true) and in a voice channel", async () => {
    const { setMuted } = await import("../../src/lib/livekitSession");
    const mockSetMuted = vi.mocked(setMuted);
    mockSetMuted.mockClear();

    mockCurrentChannelId = 7;
    testPrefs.set("pttVk", 0x20);

    let capturedCallback: ((event: { payload: boolean }) => void) | null = null;
    mockListen.mockImplementation((_event: string, cb: (e: { payload: boolean }) => void) => {
      capturedCallback = cb;
      return Promise.resolve(() => {});
    });

    await initPtt();

    expect(capturedCallback).not.toBeNull();
    capturedCallback!({ payload: true }); // key pressed

    // setMuted is reached via a dynamic import of livekitSession
    await vi.waitFor(() => {
      expect(mockSetMuted).toHaveBeenCalledWith(false);
    });
  });

  it("calls setMuted(true) when PTT is released (payload false) and in a voice channel", async () => {
    const { setMuted } = await import("../../src/lib/livekitSession");
    const mockSetMuted = vi.mocked(setMuted);
    mockSetMuted.mockClear();

    mockCurrentChannelId = 7;
    testPrefs.set("pttVk", 0x20);

    let capturedCallback: ((event: { payload: boolean }) => void) | null = null;
    mockListen.mockImplementation((_event: string, cb: (e: { payload: boolean }) => void) => {
      capturedCallback = cb;
      return Promise.resolve(() => {});
    });

    await initPtt();

    capturedCallback!({ payload: false }); // key released

    // setMuted is reached via a dynamic import of livekitSession
    await vi.waitFor(() => {
      expect(mockSetMuted).toHaveBeenCalledWith(true);
    });
  });

  it("does not call setMuted when not in a voice channel", async () => {
    const { setMuted } = await import("../../src/lib/livekitSession");
    const mockSetMuted = vi.mocked(setMuted);
    mockSetMuted.mockClear();

    mockCurrentChannelId = null; // not in a channel
    testPrefs.set("pttVk", 0x20);

    let capturedCallback: ((event: { payload: boolean }) => void) | null = null;
    mockListen.mockImplementation((_event: string, cb: (e: { payload: boolean }) => void) => {
      capturedCallback = cb;
      return Promise.resolve(() => {});
    });

    await initPtt();

    capturedCallback!({ payload: true });

    // Flush pending microtasks so a (wrong) dynamic-import path would have
    // had the chance to call setMuted before we assert it never happens.
    await new Promise((r) => setTimeout(r, 0));
    expect(mockSetMuted).not.toHaveBeenCalled();
  });

  it("does not call setMuted(false) when PTT is pressed while the user is self-muted (v006)", async () => {
    const { setMuted } = await import("../../src/lib/livekitSession");
    const mockSetMuted = vi.mocked(setMuted);
    mockSetMuted.mockClear();

    mockCurrentChannelId = 7;
    mockLocalMuted = true; // user explicitly muted themselves via the widget
    testPrefs.set("pttVk", 0x20);

    let capturedCallback: ((event: { payload: boolean }) => void) | null = null;
    mockListen.mockImplementation((_event: string, cb: (e: { payload: boolean }) => void) => {
      capturedCallback = cb;
      return Promise.resolve(() => {});
    });

    await initPtt();

    capturedCallback!({ payload: true }); // key pressed

    // Give the dynamic import a chance to resolve and (wrongly) call setMuted.
    await new Promise((r) => setTimeout(r, 0));
    expect(mockSetMuted).not.toHaveBeenCalled();
  });

  it("does not call setMuted(false) when PTT is pressed while the user is deafened (v006)", async () => {
    const { setMuted } = await import("../../src/lib/livekitSession");
    const mockSetMuted = vi.mocked(setMuted);
    mockSetMuted.mockClear();

    mockCurrentChannelId = 7;
    mockLocalDeafened = true;
    testPrefs.set("pttVk", 0x20);

    let capturedCallback: ((event: { payload: boolean }) => void) | null = null;
    mockListen.mockImplementation((_event: string, cb: (e: { payload: boolean }) => void) => {
      capturedCallback = cb;
      return Promise.resolve(() => {});
    });

    await initPtt();

    capturedCallback!({ payload: true });

    await new Promise((r) => setTimeout(r, 0));
    expect(mockSetMuted).not.toHaveBeenCalled();
  });

  it("still unmutes on press when the user is not self-muted or deafened", async () => {
    const { setMuted } = await import("../../src/lib/livekitSession");
    const mockSetMuted = vi.mocked(setMuted);
    mockSetMuted.mockClear();

    mockCurrentChannelId = 7;
    mockLocalMuted = false;
    mockLocalDeafened = false;
    testPrefs.set("pttVk", 0x20);

    let capturedCallback: ((event: { payload: boolean }) => void) | null = null;
    mockListen.mockImplementation((_event: string, cb: (e: { payload: boolean }) => void) => {
      capturedCallback = cb;
      return Promise.resolve(() => {});
    });

    await initPtt();

    capturedCallback!({ payload: true });

    await vi.waitFor(() => {
      expect(mockSetMuted).toHaveBeenCalledWith(false);
    });
  });

  it("updates pttGated in the store on press and release", async () => {
    mockCurrentChannelId = 7;
    testPrefs.set("pttVk", 0x20);

    let capturedCallback: ((event: { payload: boolean }) => void) | null = null;
    mockListen.mockImplementation((_event: string, cb: (e: { payload: boolean }) => void) => {
      capturedCallback = cb;
      return Promise.resolve(() => {});
    });

    await initPtt();
    mockSetPttGated.mockClear();

    capturedCallback!({ payload: true }); // pressed — gate open
    expect(mockSetPttGated).toHaveBeenCalledWith(false);

    capturedCallback!({ payload: false }); // released — gate closed
    expect(mockSetPttGated).toHaveBeenCalledWith(true);
  });

  it("keeps unmuting on every press even though a release writes localMuted (v006)", async () => {
    const { setMuted } = await import("../../src/lib/livekitSession");
    const mockSetMuted = vi.mocked(setMuted);
    mockSetMuted.mockReset();
    // livekitSession.setMuted() writes localMuted for every caller, PTT
    // included — the self-mute guard must not read that write back as an
    // explicit self-mute, or PTT unmutes exactly once and is dead after.
    mockSetMuted.mockImplementation((muted: boolean) => {
      mockLocalMuted = muted;
    });

    mockCurrentChannelId = 7;
    testPrefs.set("pttVk", 0x20);

    let capturedCallback: ((event: { payload: boolean }) => void) | null = null;
    mockListen.mockImplementation((_event: string, cb: (e: { payload: boolean }) => void) => {
      capturedCallback = cb;
      return Promise.resolve(() => {});
    });

    await initPtt();

    capturedCallback!({ payload: true });
    await vi.waitFor(() => expect(mockSetMuted).toHaveBeenCalledWith(false));
    capturedCallback!({ payload: false });
    await vi.waitFor(() => expect(mockLocalMuted).toBe(true));

    mockSetMuted.mockClear();
    capturedCallback!({ payload: true }); // second press
    await vi.waitFor(() => {
      expect(mockSetMuted).toHaveBeenCalledWith(false);
    });
  });

  it("stays muted on the press after the user self-mutes mid-hold (v006)", async () => {
    const { setMuted } = await import("../../src/lib/livekitSession");
    const mockSetMuted = vi.mocked(setMuted);
    mockSetMuted.mockReset();
    mockSetMuted.mockImplementation((muted: boolean) => {
      mockLocalMuted = muted;
    });

    mockCurrentChannelId = 7;
    testPrefs.set("pttVk", 0x20);

    let capturedCallback: ((event: { payload: boolean }) => void) | null = null;
    mockListen.mockImplementation((_event: string, cb: (e: { payload: boolean }) => void) => {
      capturedCallback = cb;
      return Promise.resolve(() => {});
    });

    await initPtt();

    capturedCallback!({ payload: true });
    await vi.waitFor(() => expect(mockSetMuted).toHaveBeenCalledWith(false));
    // The user hits the widget mute button while still holding the key.
    mockLocalMuted = true;
    capturedCallback!({ payload: false }); // release — mic was already muted
    await new Promise((r) => setTimeout(r, 0));

    mockSetMuted.mockClear();
    capturedCallback!({ payload: true }); // next press must not republish
    await new Promise((r) => setTimeout(r, 0));
    expect(mockSetMuted).not.toHaveBeenCalled();
  });
});

// ---------------------------------------------------------------------------
// Tests: pttOwnsMute latch is cleared by a non-PTT unmute (B1_voice_mic-7)
// ---------------------------------------------------------------------------

describe("pttOwnsMute latch reset on external unmute", () => {
  beforeEach(resetAll);

  it("stays muted on a later press after a widget unmute+re-mute clears a stale PTT-owned latch", async () => {
    const { setMuted } = await import("../../src/lib/livekitSession");
    const mockSetMuted = vi.mocked(setMuted);
    mockSetMuted.mockReset();
    mockSetMuted.mockImplementation((muted: boolean) => {
      mockLocalMuted = muted;
    });

    mockCurrentChannelId = 7;
    testPrefs.set("pttVk", 0x20);

    let capturedCallback: ((event: { payload: boolean }) => void) | null = null;
    mockListen.mockImplementation((_event: string, cb: (e: { payload: boolean }) => void) => {
      capturedCallback = cb;
      return Promise.resolve(() => {});
    });

    await initPtt();
    expect(capturedStoreListener).not.toBeNull();

    // 1. Press then release — the release is PTT's own mute, so pttOwnsMute
    //    latches true.
    capturedCallback!({ payload: true });
    await vi.waitFor(() => expect(mockSetMuted).toHaveBeenCalledWith(false));
    capturedCallback!({ payload: false });
    await vi.waitFor(() => expect(mockLocalMuted).toBe(true));

    // 2. A non-PTT path unmutes (e.g. the widget's own mic button calling
    //    livekitSession.setMuted(false) directly) — simulate both the write
    //    and the resulting store notification our subscriber reacts to.
    mockSetMuted(false);
    capturedStoreListener!({ localMuted: mockLocalMuted });

    // 3. The user then genuinely self-mutes via the same non-PTT path.
    mockSetMuted(true);
    capturedStoreListener!({ localMuted: mockLocalMuted });

    // 4. The next PTT press must not lift this genuine self-mute — the
    //    latch from step 1 must not have survived steps 2-3.
    mockSetMuted.mockClear();
    capturedCallback!({ payload: true });
    await new Promise((r) => setTimeout(r, 0));
    expect(mockSetMuted).not.toHaveBeenCalled();
  });
});

// ---------------------------------------------------------------------------
// Tests: stopPtt ungates a still-gated mic when the binding is cleared
// (B1_voice_mic-9)
// ---------------------------------------------------------------------------

describe("stopPtt ungates the mic when clearing the key mid-gate", () => {
  beforeEach(resetAll);

  it("clears pttGated and re-opens the mic when nothing else wants it muted", async () => {
    const { setMuted } = await import("../../src/lib/livekitSession");
    const mockSetMuted = vi.mocked(setMuted);
    mockSetMuted.mockClear();

    testPrefs.set("pttVk", 0x20);
    await initPtt();

    // Simulates livekitSession's join-time gate, still armed because the key
    // was never pressed before the user cleared the binding.
    mockPttGated = true;
    mockLocalMuted = false;
    mockLocalDeafened = false;

    await stopPtt();

    expect(mockSetPttGated).toHaveBeenCalledWith(false);
    await vi.waitFor(() => {
      expect(mockSetMuted).toHaveBeenCalledWith(false);
    });
  });

  it("does not force-unmute when the user is separately self-muted or deafened", async () => {
    const { setMuted } = await import("../../src/lib/livekitSession");
    const mockSetMuted = vi.mocked(setMuted);
    mockSetMuted.mockClear();

    testPrefs.set("pttVk", 0x20);
    await initPtt();

    mockPttGated = true;
    mockLocalMuted = true;

    await stopPtt();

    expect(mockSetPttGated).toHaveBeenCalledWith(false);
    await new Promise((r) => setTimeout(r, 0));
    expect(mockSetMuted).not.toHaveBeenCalled();
  });

  it("does nothing when pttGated was already false", async () => {
    testPrefs.set("pttVk", 0x20);
    await initPtt();
    mockSetPttGated.mockClear();

    mockPttGated = false;
    await stopPtt();

    expect(mockSetPttGated).not.toHaveBeenCalledWith(false);
  });

  // B1_voice_mic-11: stopPtt resets pttOwnsMute to false BEFORE calling
  // ungateMic (a mute must not outlive its binding), so ungateMic cannot
  // read the module-level latch directly — it has already been zeroed by
  // the time it runs. The value from before that reset must still decide
  // the re-open.
  it("re-opens the mic when stopPtt clears the binding after a PTT release applied the mute (bug fix)", async () => {
    const { setMuted } = await import("../../src/lib/livekitSession");
    const mockSetMuted = vi.mocked(setMuted);
    mockSetMuted.mockReset();
    mockSetMuted.mockImplementation((muted: boolean) => {
      mockLocalMuted = muted;
    });

    mockCurrentChannelId = 7;
    testPrefs.set("pttVk", 0x20);

    let capturedCallback: ((event: { payload: boolean }) => void) | null = null;
    mockListen.mockImplementation((_event: string, cb: (e: { payload: boolean }) => void) => {
      capturedCallback = cb;
      return Promise.resolve(() => {});
    });

    await initPtt();

    // Press then release: the release is PTT's own mute — pttOwnsMute
    // latches true and the store's pttGated closes.
    capturedCallback!({ payload: true });
    await vi.waitFor(() => expect(mockSetMuted).toHaveBeenCalledWith(false));
    capturedCallback!({ payload: false });
    await vi.waitFor(() => expect(mockLocalMuted).toBe(true));
    mockPttGated = true; // mirror the gate the release set in the real store
    mockLocalDeafened = false;

    mockSetMuted.mockClear();
    await stopPtt();

    expect(mockSetPttGated).toHaveBeenCalledWith(false);
    await vi.waitFor(() => {
      expect(mockSetMuted).toHaveBeenCalledWith(false);
    });
  });

  it("does not lift a user's own self-mute when stopPtt clears the binding, even after a prior PTT release owned it", async () => {
    const { setMuted } = await import("../../src/lib/livekitSession");
    const mockSetMuted = vi.mocked(setMuted);
    mockSetMuted.mockReset();
    mockSetMuted.mockImplementation((muted: boolean) => {
      mockLocalMuted = muted;
    });

    mockCurrentChannelId = 7;
    testPrefs.set("pttVk", 0x20);

    let capturedCallback: ((event: { payload: boolean }) => void) | null = null;
    mockListen.mockImplementation((_event: string, cb: (e: { payload: boolean }) => void) => {
      capturedCallback = cb;
      return Promise.resolve(() => {});
    });

    await initPtt();
    expect(capturedStoreListener).not.toBeNull();

    // A PTT release once owned the mute...
    capturedCallback!({ payload: true });
    await vi.waitFor(() => expect(mockSetMuted).toHaveBeenCalledWith(false));
    capturedCallback!({ payload: false });
    await vi.waitFor(() => expect(mockLocalMuted).toBe(true));

    // ...but the user then explicitly unmutes and re-mutes via a non-PTT
    // path (e.g. the widget's mic button), which clears the PTT-owned latch
    // — see the "latch reset" tests above. This mute is now the user's own.
    mockSetMuted(false);
    capturedStoreListener!({ localMuted: mockLocalMuted });
    mockSetMuted(true);
    capturedStoreListener!({ localMuted: mockLocalMuted });

    mockPttGated = true;
    mockLocalDeafened = false;
    mockSetMuted.mockClear();

    await stopPtt();

    expect(mockSetPttGated).toHaveBeenCalledWith(false);
    await new Promise((r) => setTimeout(r, 0));
    expect(mockSetMuted).not.toHaveBeenCalled();
  });

  it("does not re-open the mic when stopPtt clears the binding while the user is deafened, even if a PTT release owns the mute", async () => {
    const { setMuted } = await import("../../src/lib/livekitSession");
    const mockSetMuted = vi.mocked(setMuted);
    mockSetMuted.mockReset();
    mockSetMuted.mockImplementation((muted: boolean) => {
      mockLocalMuted = muted;
    });

    mockCurrentChannelId = 7;
    testPrefs.set("pttVk", 0x20);

    let capturedCallback: ((event: { payload: boolean }) => void) | null = null;
    mockListen.mockImplementation((_event: string, cb: (e: { payload: boolean }) => void) => {
      capturedCallback = cb;
      return Promise.resolve(() => {});
    });

    await initPtt();

    capturedCallback!({ payload: true });
    await vi.waitFor(() => expect(mockSetMuted).toHaveBeenCalledWith(false));
    capturedCallback!({ payload: false });
    await vi.waitFor(() => expect(mockLocalMuted).toBe(true));
    mockPttGated = true;
    mockLocalDeafened = true; // deafened independently of the PTT-owned mute

    mockSetMuted.mockClear();
    await stopPtt();

    expect(mockSetPttGated).toHaveBeenCalledWith(false);
    await new Promise((r) => setTimeout(r, 0));
    expect(mockSetMuted).not.toHaveBeenCalled();
  });
});

// ---------------------------------------------------------------------------
// Tests: 'ptt-error' listener recovers from a backend thread panic
// (B1_voice_mic-10)
// ---------------------------------------------------------------------------

describe("ptt-error event listener", () => {
  beforeEach(resetAll);

  it("registers a listener for 'ptt-error' and resets polling-live on a backend panic", async () => {
    testPrefs.set("pttVk", 0x20);

    const capturedHandlers: Record<string, (e: { payload: unknown }) => void> = {};
    mockListen.mockImplementation((event: string, cb: (e: { payload: unknown }) => void) => {
      capturedHandlers[event] = cb;
      return Promise.resolve(() => {});
    });

    await initPtt();
    mockSetPttPollingLive.mockClear();

    expect(capturedHandlers["ptt-error"]).toBeTypeOf("function");
    capturedHandlers["ptt-error"]!({ payload: "PTT thread panicked" });

    expect(mockSetPttPollingLive).toHaveBeenCalledWith(false);
  });

  it("re-opens a PTT-gated mic when the backend thread panics", async () => {
    testPrefs.set("pttVk", 0x20);
    const { setMuted } = await import("../../src/lib/livekitSession");
    const mockSetMuted = vi.mocked(setMuted);
    mockSetMuted.mockClear();

    const capturedHandlers: Record<string, (e: { payload: unknown }) => void> = {};
    mockListen.mockImplementation((event: string, cb: (e: { payload: unknown }) => void) => {
      capturedHandlers[event] = cb;
      return Promise.resolve(() => {});
    });

    await initPtt();
    mockPttGated = true;
    mockLocalMuted = false;
    mockLocalDeafened = false;

    capturedHandlers["ptt-error"]!({ payload: "PTT thread panicked" });

    expect(mockSetPttGated).toHaveBeenCalledWith(false);
    await vi.waitFor(() => {
      expect(mockSetMuted).toHaveBeenCalledWith(false);
    });
  });

  // B1_voice_mic-11: a PTT release calls setMuted(true), which writes
  // localMuted for every caller (see pttOwnsMute's doc comment) — so at the
  // moment the poller dies, localMuted is true precisely because PTT's own
  // release put it there. ungateMic's old `!localMuted` guard treated that
  // indistinguishably from a user self-mute and never re-opened the mic.
  it("re-opens the mic when the polling thread panics after a PTT release applied the mute (bug fix)", async () => {
    testPrefs.set("pttVk", 0x20);
    const { setMuted } = await import("../../src/lib/livekitSession");
    const mockSetMuted = vi.mocked(setMuted);
    mockSetMuted.mockReset();
    mockSetMuted.mockImplementation((muted: boolean) => {
      mockLocalMuted = muted;
    });

    mockCurrentChannelId = 7;
    let capturedCallback: ((event: { payload: boolean }) => void) | null = null;
    const capturedHandlers: Record<string, (e: { payload: unknown }) => void> = {};
    mockListen.mockImplementation((event: string, cb: (e: { payload: never }) => void) => {
      capturedHandlers[event] = cb as (e: { payload: unknown }) => void;
      if (event === "ptt-state")
        capturedCallback = cb as unknown as (e: { payload: boolean }) => void;
      return Promise.resolve(() => {});
    });

    await initPtt();

    // Press then release: the release is PTT's own mute — pttOwnsMute
    // latches true and the store's pttGated closes.
    capturedCallback!({ payload: true });
    await vi.waitFor(() => expect(mockSetMuted).toHaveBeenCalledWith(false));
    capturedCallback!({ payload: false });
    await vi.waitFor(() => expect(mockLocalMuted).toBe(true));
    mockPttGated = true; // mirror the gate the release set in the real store
    mockLocalDeafened = false;

    mockSetMuted.mockClear();
    capturedHandlers["ptt-error"]!({ payload: "PTT thread panicked" });

    expect(mockSetPttGated).toHaveBeenCalledWith(false);
    await vi.waitFor(() => {
      expect(mockSetMuted).toHaveBeenCalledWith(false);
    });
  });

  it("does not lift a user's own self-mute when the polling thread panics", async () => {
    testPrefs.set("pttVk", 0x20);
    const { setMuted } = await import("../../src/lib/livekitSession");
    const mockSetMuted = vi.mocked(setMuted);
    mockSetMuted.mockClear();

    const capturedHandlers: Record<string, (e: { payload: unknown }) => void> = {};
    mockListen.mockImplementation((event: string, cb: (e: { payload: unknown }) => void) => {
      capturedHandlers[event] = cb;
      return Promise.resolve(() => {});
    });

    await initPtt();
    // The user self-muted via the widget — PTT was never pressed, so this
    // mute is not PTT's to lift.
    mockPttGated = true;
    mockLocalMuted = true;
    mockLocalDeafened = false;

    capturedHandlers["ptt-error"]!({ payload: "PTT thread panicked" });

    expect(mockSetPttGated).toHaveBeenCalledWith(false);
    await new Promise((r) => setTimeout(r, 0));
    expect(mockSetMuted).not.toHaveBeenCalled();
  });

  it("does not re-open the mic on a backend panic when the user is deafened, even if a PTT release owns the mute", async () => {
    testPrefs.set("pttVk", 0x20);
    const { setMuted } = await import("../../src/lib/livekitSession");
    const mockSetMuted = vi.mocked(setMuted);
    mockSetMuted.mockReset();
    mockSetMuted.mockImplementation((muted: boolean) => {
      mockLocalMuted = muted;
    });

    mockCurrentChannelId = 7;
    let capturedCallback: ((event: { payload: boolean }) => void) | null = null;
    const capturedHandlers: Record<string, (e: { payload: unknown }) => void> = {};
    mockListen.mockImplementation((event: string, cb: (e: { payload: never }) => void) => {
      capturedHandlers[event] = cb as (e: { payload: unknown }) => void;
      if (event === "ptt-state")
        capturedCallback = cb as unknown as (e: { payload: boolean }) => void;
      return Promise.resolve(() => {});
    });

    await initPtt();

    capturedCallback!({ payload: true });
    await vi.waitFor(() => expect(mockSetMuted).toHaveBeenCalledWith(false));
    capturedCallback!({ payload: false });
    await vi.waitFor(() => expect(mockLocalMuted).toBe(true));
    mockPttGated = true;
    mockLocalDeafened = true; // deafened independently of the PTT-owned mute

    mockSetMuted.mockClear();
    capturedHandlers["ptt-error"]!({ payload: "PTT thread panicked" });

    expect(mockSetPttGated).toHaveBeenCalledWith(false);
    await new Promise((r) => setTimeout(r, 0));
    expect(mockSetMuted).not.toHaveBeenCalled();
  });
});
