import { beforeEach, describe, expect, it, vi } from 'vitest';
import { renderDevices } from '../devices';

// Mock chrome.storage.local
// @ts-expect-error test harness
globalThis.chrome = {
  storage: {
    local: {
      get: vi.fn().mockResolvedValue({ device_name: 'Chrome on Linux' }),
    },
  },
};

// Mock popup module
vi.mock('../../../shared/state', () => ({
  setState: vi.fn(),
  sendMessage: vi.fn(),
  navigate: vi.fn(),
  escapeHtml: (s: string) => s,
  popOutToTab: vi.fn(),
  isInTab: vi.fn(() => false),
  openVaultTab: vi.fn(),
}));

import { sendMessage, navigate } from '../../../shared/state';

describe('devices view', () => {
  let app: HTMLElement;

  beforeEach(() => {
    document.body.innerHTML = '<div id="app"></div>';
    app = document.getElementById('app')!;
    vi.clearAllMocks();
  });

  // The component fires list_devices + list_revoked in parallel via Promise.all,
  // so every render needs both mocked. Helper makes the per-test setup readable.
  function mockListPair(devices: unknown[], revoked: unknown[] = []): void {
    (sendMessage as ReturnType<typeof vi.fn>)
      .mockResolvedValueOnce({ ok: true, data: { devices } })
      .mockResolvedValueOnce({ ok: true, data: { revoked } });
  }

  it('renders empty state when no devices', async () => {
    mockListPair([]);

    await renderDevices(app);

    expect(app.innerHTML).toContain('No devices registered');
  });

  it('renders devices with "you" indicator on current device', async () => {
    mockListPair([
      { name: 'Chrome on Linux', public_key: 'abc', added_at: 1000 },
      { name: 'CLI', public_key: 'def', added_at: 500 },
    ]);

    await renderDevices(app);

    expect(app.innerHTML).toContain('Chrome on Linux');
    expect(app.innerHTML).toContain('← you');
    expect(app.innerHTML).toContain('CLI');
    // Current device should not have revoke button
    const rows = app.querySelectorAll('.device-row');
    expect(rows[0].querySelector('[data-revoke]')).toBeNull();
    expect(rows[1].querySelector('[data-revoke]')).not.toBeNull();
  });

  it('shows unregistered banner when current device not in list', async () => {
    (chrome.storage.local.get as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ device_name: 'Unknown' });
    mockListPair([{ name: 'CLI', public_key: 'abc', added_at: 1000 }]);

    await renderDevices(app);

    expect(app.innerHTML).toContain("This device isn't registered");
  });

  it('back button navigates to list', async () => {
    mockListPair([]);

    await renderDevices(app);
    app.querySelector<HTMLButtonElement>('#back-btn')?.click();

    expect(navigate).toHaveBeenCalledWith('list');
  });

  it('clicking register button reveals an inline name input', async () => {
    (chrome.storage.local.get as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ device_name: 'Unknown' });
    mockListPair([{ name: 'CLI', public_key: 'k', added_at: 1 }]);

    await renderDevices(app);
    app.querySelector<HTMLButtonElement>('#register-btn')!.click();

    expect(app.querySelector<HTMLInputElement>('#register-name-input')).not.toBeNull();
    expect(app.querySelector<HTMLButtonElement>('#register-confirm-btn')).not.toBeNull();
  });

  // Plan C Phase 5 — defensive Promise.allSettled:
  // a rejected secondary feed (list_revoked) should not kill the whole render.
  it('renders devices when revoked list fails (load-error slot shown)', async () => {
    (sendMessage as ReturnType<typeof vi.fn>)
      .mockResolvedValueOnce({ ok: true, data: { devices: [{ name: 'CLI', public_key: 'k', added_at: 1 }] } })
      .mockRejectedValueOnce(new Error('boom'));

    await renderDevices(app);

    // Primary list still rendered.
    expect(app.innerHTML).toContain('CLI');
    // Inline fallback slot present.
    expect(app.innerHTML).toContain("Couldn't load revoked devices");
  });

  it('renders devices when revoked list returns {ok:false}', async () => {
    (sendMessage as ReturnType<typeof vi.fn>)
      .mockResolvedValueOnce({ ok: true, data: { devices: [{ name: 'CLI', public_key: 'k', added_at: 1 }] } })
      .mockResolvedValueOnce({ ok: false, error: 'list_revoked_failed' });

    await renderDevices(app);

    expect(app.innerHTML).toContain('CLI');
    expect(app.innerHTML).toContain("Couldn't load revoked devices");
  });

  it('confirming register sends register_this_device with the entered name', async () => {
    (chrome.storage.local.get as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ device_name: 'Unknown' });
    // Initial render: list_devices + list_revoked.
    mockListPair([{ name: 'CLI', public_key: 'k', added_at: 1 }]);
    // register_this_device.
    (sendMessage as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ ok: true });
    // Re-render: list_devices + list_revoked.
    mockListPair([
      { name: 'CLI', public_key: 'k', added_at: 1 },
      { name: 'Test Browser', public_key: 'q', added_at: 2 },
    ]);
    // Re-render also re-reads device_name from storage.
    (chrome.storage.local.get as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ device_name: 'Test Browser' });

    await renderDevices(app);
    app.querySelector<HTMLButtonElement>('#register-btn')!.click();
    const input = app.querySelector<HTMLInputElement>('#register-name-input')!;
    input.value = 'Test Browser';
    app.querySelector<HTMLButtonElement>('#register-confirm-btn')!.click();
    // Wait a microtask for the async handler to run.
    await new Promise((r) => setTimeout(r, 0));
    await new Promise((r) => setTimeout(r, 0));

    expect(sendMessage).toHaveBeenCalledWith({ type: 'register_this_device', name: 'Test Browser' });
  });
});
