import { describe, expect, it } from 'vitest';
import { State, type SessionInfo } from '@agentdeck/shared';
import { SessionSlotManager, type DeckLayout } from '../session-slot-manager.js';

const SD_PLUS_LAYOUT: DeckLayout = {
  columns: 4,
  rows: 2,
  keyCount: 8,
  family: 'streamdeckplus',
};

// Classic Stream Deck (15 keys, no encoder) — carries usage on the last 2 keys.
const SD_CLASSIC_LAYOUT: DeckLayout = {
  columns: 5,
  rows: 3,
  keyCount: 15,
  family: 'streamdeck',
};

function makeSession(overrides: Partial<SessionInfo> = {}): SessionInfo {
  return {
    id: 'session-1',
    port: 9121,
    projectName: 'AgentDeck',
    agentType: 'claude-code',
    alive: true,
    state: State.IDLE,
    modelName: 'opus-4',
    effortLevel: 'high',
    ...overrides,
  };
}

describe('SessionSlotManager detail layout', () => {
  it('re-points detail focus onto the codex fold representative when the focused thread is absorbed', () => {
    const manager = new SessionSlotManager();
    manager.updateSessions([
      makeSession({ id: 'codex:old', agentType: 'codex-cli', state: State.IDLE, startedAt: '2026-04-11T10:00:00Z' }),
    ], false);
    manager.enterDetailView('codex:old');
    expect(manager.focusedSessionId).toBe('codex:old');

    manager.updateSessions([
      makeSession({ id: 'codex:old', agentType: 'codex-cli', state: State.IDLE, startedAt: '2026-04-11T10:00:00Z' }),
      makeSession({ id: 'codex:new', agentType: 'codex-cli', state: State.PROCESSING, startedAt: '2026-04-11T10:02:00Z' }),
    ], false);

    expect(manager.view).toBe('detail');
    expect(manager.focusedSessionId).toBe('codex:new');
    expect(manager.getFocusedSession()?.foldedSessionIds).toContain('codex:old');
  });

  it('exits detail view when the focused session is gone with no fold successor', () => {
    const manager = new SessionSlotManager();
    manager.updateSessions([
      makeSession({ id: 'claude:1', agentType: 'claude-code', state: State.IDLE }),
    ], false);
    manager.enterDetailView('claude:1');

    manager.updateSessions([], false);

    expect(manager.view).toBe('list');
    expect(manager.focusedSessionId).toBeNull();
  });

  it('folds codex companion threads by project before slot assignment', () => {
    const manager = new SessionSlotManager();
    manager.updateSessions([
      makeSession({
        id: 'codex:old',
        agentType: 'codex-cli',
        state: State.IDLE,
        startedAt: '2026-04-11T10:00:00Z',
      }),
      makeSession({
        id: 'codex:new',
        agentType: 'codex-cli',
        state: State.PROCESSING,
        currentTool: 'exec',
        startedAt: '2026-04-11T10:02:00Z',
      }),
      makeSession({
        id: 'claude:1',
        agentType: 'claude-code',
        state: State.IDLE,
        startedAt: '2026-04-11T10:01:00Z',
      }),
    ], false);

    expect(manager.sessions.map(s => s.id)).toEqual(['claude:1', 'codex:new']);
    expect(manager.sessions[1]).toMatchObject({
      groupSize: 2,
      foldedSessionIds: ['codex:old', 'codex:new'],
      currentTool: 'exec',
      state: State.PROCESSING,
    });
  });

  it('renders connected no-session list as status cards instead of text-only empty buttons', () => {
    const manager = new SessionSlotManager();
    manager.updateSessions([], false);

    expect(manager.getSlotConfig(0, SD_PLUS_LAYOUT)).toMatchObject({
      type: 'status',
      label: 'HUB READY',
      subtitle: 'CONNECTED',
      icon: 'hub',
    });
    expect(manager.getSlotConfig(1, SD_PLUS_LAYOUT)).toMatchObject({
      type: 'status',
      label: 'NO SESSION',
      subtitle: 'WAITING',
      icon: 'no-session',
    });
    expect(manager.getSlotConfig(2, SD_PLUS_LAYOUT)).toMatchObject({
      type: 'status',
      label: 'AgentDeck',
      subtitle: 'IDLE',
      icon: 'agentdeck',
    });
  });

  it('puts processing tool info before OpenClaw presets', () => {
    const manager = new SessionSlotManager();
    manager.updateSessions([
      makeSession({
        id: 'openclaw',
        agentType: 'openclaw',
        state: State.PROCESSING,
        modelName: 'gpt-5',
      }),
    ], true);
    manager.enterDetailView('openclaw');
    manager.updateDetailState(State.PROCESSING, [], 'logs.tail', 'tail latest logs', undefined, 'gpt-5');

    expect(manager.getSlotConfig(2, SD_PLUS_LAYOUT)).toMatchObject({
      type: 'status',
      label: 'logs.tail',
      subtitle: 'tail latest logs',
      icon: 'tool',
    });
    expect(manager.getSlotConfig(3, SD_PLUS_LAYOUT)).toMatchObject({
      type: 'preset',
      preset: { label: 'STATUS' },
    });
    expect(manager.getSlotConfig(4, SD_PLUS_LAYOUT)).toMatchObject({
      type: 'preset',
      preset: { label: 'MODEL' },
    });
    expect(manager.getSlotConfig(5, SD_PLUS_LAYOUT)).toMatchObject({
      type: 'preset',
      preset: { label: 'GATEWAY' },
    });
  });

  it('keeps a processing status tile even before tool metadata arrives', () => {
    const manager = new SessionSlotManager();
    manager.updateSessions([
      makeSession({
        id: 'openclaw',
        agentType: 'openclaw',
        state: State.PROCESSING,
      }),
    ], true);
    manager.enterDetailView('openclaw');
    manager.updateDetailState(State.PROCESSING, []);

    expect(manager.getSlotConfig(2, SD_PLUS_LAYOUT)).toMatchObject({
      type: 'status',
      label: 'ROUTING',
      subtitle: 'running',
      icon: 'tool',
    });
    expect(manager.getSlotConfig(3, SD_PLUS_LAYOUT)).toMatchObject({
      type: 'preset',
      preset: { label: 'STATUS' },
    });
  });

  it('aliases the model name on detail MODEL surfaces (status card + OpenClaw preset)', () => {
    // Claude Code IDLE: MODEL status card subtitle uses the alias, not the raw upstream id.
    const cc = new SessionSlotManager();
    cc.updateSessions([makeSession({ modelName: 'claude-sonnet-4-6', effortLevel: undefined })], false);
    cc.enterDetailView('session-1');
    cc.updateDetailState(State.IDLE, [], undefined, undefined, undefined, 'claude-sonnet-4-6');
    const ccModelCard = [0, 1, 2, 3, 4, 5, 6, 7]
      .map(i => cc.getSlotConfig(i, SD_PLUS_LAYOUT))
      .find(c => c.type === 'status' && c.label === 'MODEL');
    expect(ccModelCard).toMatchObject({ type: 'status', label: 'MODEL', subtitle: 'sonnet 4.6' });

    // OpenClaw IDLE: model preset subtitle is aliased too.
    const oc = new SessionSlotManager();
    oc.updateSessions([makeSession({ id: 'oc', agentType: 'openclaw', modelName: 'claude-opus-4-7' })], true);
    oc.enterDetailView('oc');
    oc.updateDetailState(State.IDLE, [], undefined, undefined, undefined, 'claude-opus-4-7');
    const ocModelPreset = [0, 1, 2, 3, 4, 5, 6, 7]
      .map(i => oc.getSlotConfig(i, SD_PLUS_LAYOUT))
      .find(c => c.type === 'preset' && c.preset?.label === 'MODEL');
    expect(ocModelPreset?.preset?.subtitle).toBe('opus 4.7');
  });

  it('renders the MODEL tile exactly once in Claude PROCESSING detail (no duplicate)', () => {
    const manager = new SessionSlotManager();
    manager.updateSessions([makeSession({ state: State.PROCESSING, modelName: 'claude-opus-4-8' })], false);
    manager.enterDetailView('session-1');
    manager.updateDetailState(State.PROCESSING, [], 'Edit', 'cli.ts', undefined, 'claude-opus-4-8', 'acceptEdits');

    const labels = [0, 1, 2, 3, 4, 5, 6, 7]
      .map(i => manager.getSlotConfig(i, SD_PLUS_LAYOUT))
      .filter(c => c.type === 'status' && c.label === 'MODEL');
    expect(labels).toHaveLength(1);
  });

  it('does not duplicate MODEL on OpenClaw idle (preset + status card)', () => {
    const manager = new SessionSlotManager();
    manager.updateSessions([makeSession({ id: 'oc', agentType: 'openclaw', modelName: 'gpt-5' })], true);
    manager.enterDetailView('oc');
    manager.updateDetailState(State.IDLE, [], undefined, undefined, undefined, 'gpt-5');

    const modelSurfaces = [0, 1, 2, 3, 4, 5, 6, 7]
      .map(i => manager.getSlotConfig(i, SD_PLUS_LAYOUT))
      .filter(c => (c.type === 'status' && c.label === 'MODEL') || (c.type === 'preset' && c.preset?.label === 'MODEL'));
    expect(modelSurfaces).toHaveLength(1);
    expect(modelSurfaces[0].type).toBe('preset');
  });

  it('does not render a READY/idle tile while a Claude session is PROCESSING', () => {
    const manager = new SessionSlotManager();
    manager.updateSessions([makeSession({ state: State.PROCESSING, modelName: 'claude-opus-4-8' })], false);
    manager.enterDetailView('session-1');
    manager.updateDetailState(State.PROCESSING, [], 'Edit', 'cli.ts', undefined, 'claude-opus-4-8', 'acceptEdits');

    const idleTiles = [0, 1, 2, 3, 4, 5, 6, 7]
      .map(i => manager.getSlotConfig(i, SD_PLUS_LAYOUT))
      .filter(c => c.type === 'status' && (c.label === 'READY' || c.subtitle === 'idle'));
    expect(idleTiles).toHaveLength(0);
  });

  it('does not render a STANDBY/idle tile while an OpenClaw session is PROCESSING', () => {
    const manager = new SessionSlotManager();
    manager.updateSessions([makeSession({ id: 'oc', agentType: 'openclaw', state: State.PROCESSING, modelName: 'gpt-5' })], true);
    manager.enterDetailView('oc');
    manager.updateDetailState(State.PROCESSING, [], 'route', undefined, undefined, 'gpt-5');

    const idleTiles = [0, 1, 2, 3, 4, 5, 6, 7]
      .map(i => manager.getSlotConfig(i, SD_PLUS_LAYOUT))
      .filter(c => c.type === 'status' && (c.label === 'STANDBY' || c.subtitle === 'idle'));
    expect(idleTiles).toHaveLength(0);
  });

  it('uses actual parser options and reserves MORE only when awaiting overflow exists', () => {
    const manager = new SessionSlotManager();
    manager.updateSessions([makeSession({ state: State.AWAITING_OPTION })], false);
    manager.enterDetailView('session-1');
    manager.updateDetailState(State.AWAITING_OPTION, [
      { index: 0, label: 'Yes' },
      { index: 1, label: 'No' },
      { index: 2, label: 'Always allow' },
      { index: 3, label: 'Deny' },
      { index: 4, label: 'Explain' },
    ]);

    expect(manager.getSlotConfig(2, SD_PLUS_LAYOUT)).toMatchObject({ type: 'option', optionIndex: 0 });
    expect(manager.getSlotConfig(5, SD_PLUS_LAYOUT)).toMatchObject({ type: 'option', optionIndex: 3 });
    expect(manager.getSlotConfig(6, SD_PLUS_LAYOUT)).toMatchObject({ type: 'next-page', label: '1/2' });
    expect(manager.getSlotConfig(7, SD_PLUS_LAYOUT)).toMatchObject({ type: 'esc', label: 'active' });
  });
});

// Phase 2: SD+ relocates AWAITING option/permission selection AND the suggested-
// prompt quick-send onto the keypad (encoders now show usage permanently).
describe('SD+ keypad relocation (Phase 2)', () => {
  it('AWAITING permission renders selectable option buttons + ESC on the SD+ keypad', () => {
    const manager = new SessionSlotManager();
    manager.updateSessions([makeSession({ state: State.AWAITING_PERMISSION })], false);
    manager.enterDetailView('session-1');
    manager.updateDetailState(State.AWAITING_PERMISSION, [
      { index: 0, label: 'Yes', shortcut: 'y' },
      { index: 1, label: "Yes, and don't ask again", shortcut: 'a' },
      { index: 2, label: 'No', shortcut: 'n' },
    ]);

    // Options are pressable keypad buttons that dispatch select-option.
    expect(manager.getSlotConfig(2, SD_PLUS_LAYOUT)).toMatchObject({ type: 'option', optionIndex: 0 });
    expect(manager.handleSlotPress(2, SD_PLUS_LAYOUT)).toMatchObject({ action: 'select-option', optionIndex: 0 });
    expect(manager.handleSlotPress(4, SD_PLUS_LAYOUT)).toMatchObject({ action: 'select-option', optionIndex: 2 });
    // ESC remains on the last key to cancel.
    expect(manager.getSlotConfig(7, SD_PLUS_LAYOUT)).toMatchObject({ type: 'esc', label: 'active' });
    expect(manager.handleSlotPress(7, SD_PLUS_LAYOUT)).toMatchObject({ action: 'esc' });
  });

  it('shows a SUGGESTED quick-send button leading the IDLE detail content on SD+', () => {
    const manager = new SessionSlotManager();
    manager.updateSessions([makeSession({ state: State.IDLE })], false);
    manager.enterDetailView('session-1');
    manager.updateDetailState(State.IDLE, [], undefined, undefined, undefined, 'claude-opus-4-8', undefined, undefined, 'run the test suite');

    // Leading content slot (slot 2 on SD+) = the suggested-prompt preset.
    expect(manager.getSlotConfig(2, SD_PLUS_LAYOUT)).toMatchObject({
      type: 'preset',
      preset: { label: 'SUGGESTED', prompt: 'run the test suite' },
    });
    // Pressing it sends the suggestion as a prompt.
    expect(manager.handleSlotPress(2, SD_PLUS_LAYOUT)).toMatchObject({ action: 'send-prompt', promptText: 'run the test suite' });
    // The CC quick-action presets follow (shifted by one).
    expect(manager.getSlotConfig(3, SD_PLUS_LAYOUT)).toMatchObject({ type: 'preset', preset: { label: 'GO ON' } });
  });

  it('does NOT add a SUGGESTED button on classic Stream Deck (behavior unchanged)', () => {
    const manager = new SessionSlotManager();
    manager.updateSessions([makeSession({ state: State.IDLE })], false);
    manager.enterDetailView('session-1');
    manager.updateDetailState(State.IDLE, [], undefined, undefined, undefined, 'claude-opus-4-8', undefined, undefined, 'run the test suite');

    const suggested = Array.from({ length: 15 }, (_, i) => manager.getSlotConfig(i, SD_CLASSIC_LAYOUT))
      .filter(c => c.type === 'preset' && c.preset?.label === 'SUGGESTED');
    expect(suggested).toHaveLength(0);
    // Classic IDLE detail still leads with the CC quick-action presets.
    const firstContent = manager.getSlotConfig(2, SD_CLASSIC_LAYOUT);
    expect(firstContent).toMatchObject({ type: 'preset', preset: { label: 'GO ON' } });
  });

  it('drops the SUGGESTED button when the session leaves IDLE', () => {
    const manager = new SessionSlotManager();
    manager.updateSessions([makeSession({ state: State.IDLE })], false);
    manager.enterDetailView('session-1');
    manager.updateDetailState(State.IDLE, [], undefined, undefined, undefined, 'm', undefined, undefined, 'do the thing');
    expect(manager.getSlotConfig(2, SD_PLUS_LAYOUT)).toMatchObject({ preset: { label: 'SUGGESTED' } });

    // A PROCESSING update with a stale suggestion must not surface the button.
    manager.updateDetailState(State.PROCESSING, [], 'Edit', 'x.ts', undefined, 'm', undefined, undefined, 'do the thing');
    const suggested = [0, 1, 2, 3, 4, 5, 6, 7]
      .map(i => manager.getSlotConfig(i, SD_PLUS_LAYOUT))
      .filter(c => c.type === 'preset' && c.preset?.label === 'SUGGESTED');
    expect(suggested).toHaveLength(0);
  });
});

describe('SessionSlotManager list-view usage tiles', () => {
  const fewSessions = (n: number) =>
    Array.from({ length: n }, (_, i) => makeSession({ id: `s${i}`, port: 9121 + i, projectName: `p${i}` }));

  const CODEX_LIMITS = {
    primary: { usedPercent: 30, windowMinutes: 300, resetsAt: '2099-01-01T00:00:00Z' },
    secondary: { usedPercent: 12, windowMinutes: 10080, resetsAt: '2099-01-08T00:00:00Z' },
  };

  it('pins Claude 5H/7D to the last two keys of a classic Stream Deck (no Codex)', () => {
    const manager = new SessionSlotManager();
    manager.updateUsage({ fiveHourPercent: 42, sevenDayPercent: 17 });
    manager.updateSessions(fewSessions(3), false);

    expect(manager.getSlotConfig(14, SD_CLASSIC_LAYOUT)).toMatchObject({ type: 'usage', usageLabel: '7D', usagePercent: 17, usageKnown: true, usageAgent: 'claude', usageWindow: '7d' });
    expect(manager.getSlotConfig(13, SD_CLASSIC_LAYOUT)).toMatchObject({ type: 'usage', usageLabel: '5H', usagePercent: 42, usageKnown: true, usageAgent: 'claude', usageWindow: '5h' });
    // Sessions fill the front keys.
    expect(manager.getSlotConfig(0, SD_CLASSIC_LAYOUT).type).toBe('session');
  });

  it('reserves 4 keys (Claude 5h/7d + Codex 5h/7d), left-aligned, when both agents report quota', () => {
    const manager = new SessionSlotManager();
    manager.updateUsage({
      fiveHourPercent: 42,
      fiveHourResetsAt: '2099-01-01T00:00:00Z',
      sevenDayPercent: 17,
      sevenDayResetsAt: '2099-01-02T00:00:00Z',
      codexRateLimits: CODEX_LIMITS,
    });
    manager.updateSessions(fewSessions(3), false);

    // Block is the last 4 keys: 11=C5h, 12=C7d, 13=CX5h, 14=CX7d.
    expect(manager.getSlotConfig(11, SD_CLASSIC_LAYOUT)).toMatchObject({ type: 'usage', usageLabel: '5H', usageAgent: 'claude', usageWindow: '5h', usagePercent: 42 });
    expect(manager.getSlotConfig(12, SD_CLASSIC_LAYOUT)).toMatchObject({ type: 'usage', usageLabel: '7D', usageAgent: 'claude', usageWindow: '7d', usagePercent: 17 });
    // Codex windows use the same short labels — agent rides usageAgent/brand dot.
    expect(manager.getSlotConfig(13, SD_CLASSIC_LAYOUT)).toMatchObject({ type: 'usage', usageLabel: '5H', usageAgent: 'codex', usageWindow: '5h', usagePercent: 30 });
    expect(manager.getSlotConfig(14, SD_CLASSIC_LAYOUT)).toMatchObject({ type: 'usage', usageLabel: '7D', usageAgent: 'codex', usageWindow: '7d', usagePercent: 12 });
    // Sessions still fill the front keys (before the reserved block).
    expect(manager.getSlotConfig(0, SD_CLASSIC_LAYOUT).type).toBe('session');
  });

  it('hides Codex tiles when only Claude reports quota (reserve 2, not 4)', () => {
    const manager = new SessionSlotManager();
    manager.updateUsage({ fiveHourPercent: 42, sevenDayPercent: 17 });
    manager.updateSessions(fewSessions(3), false);

    const types = Array.from({ length: 15 }, (_, i) => manager.getSlotConfig(i, SD_CLASSIC_LAYOUT).type);
    expect(types.filter((t) => t === 'usage')).toHaveLength(2);
    // No Codex tiles anywhere (agent identity rides usageAgent, not a label).
    const agents = Array.from({ length: 15 }, (_, i) => manager.getSlotConfig(i, SD_CLASSIC_LAYOUT).usageAgent);
    expect(agents.some((a) => a === 'codex')).toBe(false);
  });

  it('hides Claude tiles when only Codex reports quota (reserve 2 Codex tiles)', () => {
    const manager = new SessionSlotManager();
    manager.updateUsage({ codexRateLimits: CODEX_LIMITS });
    manager.updateSessions(fewSessions(3), false);

    expect(manager.getSlotConfig(13, SD_CLASSIC_LAYOUT)).toMatchObject({ type: 'usage', usageLabel: '5H', usageAgent: 'codex' });
    expect(manager.getSlotConfig(14, SD_CLASSIC_LAYOUT)).toMatchObject({ type: 'usage', usageLabel: '7D', usageAgent: 'codex' });
    const types = Array.from({ length: 15 }, (_, i) => manager.getSlotConfig(i, SD_CLASSIC_LAYOUT).type);
    expect(types.filter((t) => t === 'usage')).toHaveLength(2);
  });

  it('does NOT reserve usage on Stream Deck+ (encoder carries usage)', () => {
    const manager = new SessionSlotManager();
    manager.updateUsage({ fiveHourPercent: 42, sevenDayPercent: 17, codexRateLimits: CODEX_LIMITS });
    manager.updateSessions(fewSessions(3), false);

    for (let slot = 0; slot < 8; slot++) {
      expect(manager.getSlotConfig(slot, SD_PLUS_LAYOUT).type).not.toBe('usage');
    }
  });

  it('reserves NO usage keys when no quota was fed (hide-if-absent)', () => {
    const manager = new SessionSlotManager();
    manager.updateSessions(fewSessions(1), false);
    const types = Array.from({ length: 15 }, (_, i) => manager.getSlotConfig(i, SD_CLASSIC_LAYOUT).type);
    expect(types.filter((t) => t === 'usage')).toHaveLength(0);
  });

  it('drops Claude tiles when its quota goes stale (hide-if-absent on stale)', () => {
    const manager = new SessionSlotManager();
    manager.updateUsage({ fiveHourPercent: 42, sevenDayPercent: 17 });
    manager.updateSessions(fewSessions(3), false);
    expect(manager.getSlotConfig(13, SD_CLASSIC_LAYOUT).type).toBe('usage');

    manager.updateUsage({ fiveHourPercent: 42, sevenDayPercent: 17, usageStale: true });
    const types = Array.from({ length: 15 }, (_, i) => manager.getSlotConfig(i, SD_CLASSIC_LAYOUT).type);
    expect(types.filter((t) => t === 'usage')).toHaveLength(0);
  });

  it('fits 13 sessions on a classic deck without paging (15 keys − 2 usage)', () => {
    const manager = new SessionSlotManager();
    manager.updateUsage({ fiveHourPercent: 1, sevenDayPercent: 2 });
    manager.updateSessions(fewSessions(13), false);

    const types = Array.from({ length: 15 }, (_, i) => manager.getSlotConfig(i, SD_CLASSIC_LAYOUT).type);
    expect(types.filter((t) => t === 'session')).toHaveLength(13);
    expect(types.filter((t) => t === 'next-page')).toHaveLength(0);
    expect(types.filter((t) => t === 'usage')).toHaveLength(2);
  });

  it('paginates over capacity: NEXT→ at slot 12, usage at 13/14', () => {
    const manager = new SessionSlotManager();
    manager.updateUsage({ fiveHourPercent: 1, sevenDayPercent: 2 });
    manager.updateSessions(fewSessions(15), false); // > 13 cap → 12/page + NEXT

    expect(manager.getSlotConfig(12, SD_CLASSIC_LAYOUT)).toMatchObject({ type: 'next-page', label: '1/2' });
    expect(manager.getSlotConfig(13, SD_CLASSIC_LAYOUT).type).toBe('usage');
    expect(manager.getSlotConfig(14, SD_CLASSIC_LAYOUT).type).toBe('usage');
    const sessionCount = Array.from({ length: 15 }, (_, i) => manager.getSlotConfig(i, SD_CLASSIC_LAYOUT).type)
      .filter((t) => t === 'session').length;
    expect(sessionCount).toBe(12);
  });

  it('repositions NEXT→ ahead of a 4-key usage block when paginating', () => {
    const manager = new SessionSlotManager();
    manager.updateUsage({ fiveHourPercent: 1, sevenDayPercent: 2, codexRateLimits: CODEX_LIMITS });
    manager.updateSessions(fewSessions(15), false); // > (15 − 4) cap → paginate

    // NEXT→ sits just before the reserved block: keyCount(15) − 1 − reserve(4) = 10.
    expect(manager.getSlotConfig(10, SD_CLASSIC_LAYOUT)).toMatchObject({ type: 'next-page' });
    for (const s of [11, 12, 13, 14]) {
      expect(manager.getSlotConfig(s, SD_CLASSIC_LAYOUT).type).toBe('usage');
    }
    const sessionCount = Array.from({ length: 15 }, (_, i) => manager.getSlotConfig(i, SD_CLASSIC_LAYOUT).type)
      .filter((t) => t === 'session').length;
    expect(sessionCount).toBe(10); // cap(11) − 1 NEXT key
  });

  it('pressing a usage tile resolves to refresh-usage', () => {
    const manager = new SessionSlotManager();
    manager.updateUsage({ fiveHourPercent: 5, sevenDayPercent: 6 });
    manager.updateSessions(fewSessions(2), false);
    expect(manager.handleSlotPress(14, SD_CLASSIC_LAYOUT)).toEqual({ action: 'refresh-usage' });
  });

  it('shows usage tiles even with zero sessions', () => {
    const manager = new SessionSlotManager();
    manager.updateUsage({ fiveHourPercent: 5, sevenDayPercent: 6 });
    manager.updateSessions([], false);
    expect(manager.getSlotConfig(13, SD_CLASSIC_LAYOUT).type).toBe('usage');
    expect(manager.getSlotConfig(14, SD_CLASSIC_LAYOUT).type).toBe('usage');
    // Status cards still render on the front keys.
    expect(manager.getSlotConfig(0, SD_CLASSIC_LAYOUT)).toMatchObject({ type: 'status', label: 'HUB READY' });
  });
});
