import { describe, it, expect, vi, beforeEach } from 'vitest';
import { wireTotpPreview, wireTotpQr } from '../totp-tools';

describe('wireTotpPreview', () => {
  let form: HTMLElement;
  let sendMessage: ReturnType<typeof vi.fn>;

  beforeEach(() => {
    form = document.createElement('div');
    form.innerHTML = `
      <input id="f-totp" type="text" value="" />
      <div id="totp-preview-row" class="totp-preview" hidden>
        <span class="totp-code">…</span>
        <span class="totp-countdown">…</span>
      </div>
    `;
    document.body.appendChild(form);
    sendMessage = vi.fn();
    vi.useFakeTimers();
  });

  it('shows preview when secret is valid base32', async () => {
    sendMessage.mockResolvedValue({ ok: true, data: { code: '492837', expires_at: Math.floor(Date.now() / 1000) + 23 } });
    const teardown = wireTotpPreview(form, { sendMessage });
    const input = form.querySelector('#f-totp') as HTMLInputElement;
    input.value = 'JBSWY3DPEHPK3PXP';
    input.dispatchEvent(new Event('input'));
    await vi.advanceTimersByTimeAsync(50);
    const row = form.querySelector('#totp-preview-row') as HTMLElement;
    expect(row.hidden).toBe(false);
    expect(row.querySelector('.totp-code')?.textContent).toBe('492 837');
    expect(row.querySelector('.totp-countdown')?.textContent).toMatch(/\d+s/);
    teardown();
  });

  it('hides preview when secret is too short', async () => {
    const teardown = wireTotpPreview(form, { sendMessage });
    const input = form.querySelector('#f-totp') as HTMLInputElement;
    input.value = 'TOOSHORT';
    input.dispatchEvent(new Event('input'));
    await vi.advanceTimersByTimeAsync(50);
    const row = form.querySelector('#totp-preview-row') as HTMLElement;
    expect(row.hidden).toBe(true);
    expect(sendMessage).not.toHaveBeenCalled();
    teardown();
  });

  it('teardown stops the interval', async () => {
    sendMessage.mockResolvedValue({ ok: true, data: { code: '111111', expires_at: Math.floor(Date.now() / 1000) + 30 } });
    const teardown = wireTotpPreview(form, { sendMessage });
    const input = form.querySelector('#f-totp') as HTMLInputElement;
    input.value = 'JBSWY3DPEHPK3PXP';
    input.dispatchEvent(new Event('input'));
    await vi.advanceTimersByTimeAsync(50);
    const callsBefore = sendMessage.mock.calls.length;
    teardown();
    await vi.advanceTimersByTimeAsync(2000);
    expect(sendMessage.mock.calls.length).toBe(callsBefore);
  });
});

describe('wireTotpQr', () => {
  let form: HTMLElement;
  let decodeQrFromBlob: ReturnType<typeof vi.fn>;

  beforeEach(() => {
    vi.useRealTimers();
    form = document.createElement('div');
    form.innerHTML = `
      <input id="f-totp" type="text" value="" />
      <button id="totp-qr-btn" class="glyph-btn" type="button" title="QR">◫</button>
      <div id="totp-qr-panel" class="totp-qr-panel" hidden>
        <input id="totp-qr-file" type="file" accept="image/*" />
        <div id="totp-qr-error" class="totp-qr-error"></div>
      </div>
    `;
    document.body.appendChild(form);
    decodeQrFromBlob = vi.fn();
  });

  it('toggles the panel on button click', () => {
    wireTotpQr(form, { decodeQrFromBlob });
    const btn = form.querySelector('#totp-qr-btn') as HTMLButtonElement;
    const panel = form.querySelector('#totp-qr-panel') as HTMLElement;
    expect(panel.hidden).toBe(true);
    btn.click();
    expect(panel.hidden).toBe(false);
    btn.click();
    expect(panel.hidden).toBe(true);
  });

  it('fills f-totp on successful decode of otpauth:// URI', async () => {
    decodeQrFromBlob.mockResolvedValue('otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example');
    wireTotpQr(form, { decodeQrFromBlob });
    const fileInput = form.querySelector('#totp-qr-file') as HTMLInputElement;
    const fakeFile = new File(['x'], 'qr.png', { type: 'image/png' });
    Object.defineProperty(fileInput, 'files', { value: [fakeFile] });
    fileInput.dispatchEvent(new Event('change'));
    await Promise.resolve(); await Promise.resolve();
    expect((form.querySelector('#f-totp') as HTMLInputElement).value).toBe('JBSWY3DPEHPK3PXP');
  });

  it('shows error when QR decodes but is not otpauth://', async () => {
    decodeQrFromBlob.mockResolvedValue('https://example.com/');
    wireTotpQr(form, { decodeQrFromBlob });
    const fileInput = form.querySelector('#totp-qr-file') as HTMLInputElement;
    Object.defineProperty(fileInput, 'files', { value: [new File(['x'], 'x.png', { type: 'image/png' })] });
    fileInput.dispatchEvent(new Event('change'));
    await Promise.resolve(); await Promise.resolve();
    const err = form.querySelector('#totp-qr-error') as HTMLElement;
    expect(err.textContent).toMatch(/not a totp uri/i);
    expect((form.querySelector('#f-totp') as HTMLInputElement).value).toBe('');
  });

  it('shows error when decode returns null (no QR found)', async () => {
    decodeQrFromBlob.mockResolvedValue(null);
    wireTotpQr(form, { decodeQrFromBlob });
    const fileInput = form.querySelector('#totp-qr-file') as HTMLInputElement;
    Object.defineProperty(fileInput, 'files', { value: [new File(['x'], 'x.png', { type: 'image/png' })] });
    fileInput.dispatchEvent(new Event('change'));
    await Promise.resolve(); await Promise.resolve();
    const err = form.querySelector('#totp-qr-error') as HTMLElement;
    expect(err.textContent).toMatch(/no qr found/i);
  });
});
