import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { PairingManager, type PairingStore } from './pairing.js';

describe('PairingManager', () => {
  let manager: PairingManager;

  beforeEach(() => {
    manager = new PairingManager({
      enabled: true,
      codeTtl: 300_000, // 5 minutes
      codeLength: 6,
    });
  });

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

  it('generateCode returns a code of the configured length', () => {
    const code = manager.generateCode();
    expect(code).toHaveLength(6);
    expect(code).toMatch(/^[0-9A-F]+$/);
  });

  it('generateCode with a label stores the label', () => {
    const code = manager.generateCode('for alice');
    const pending = manager.listPendingCodes();
    expect(pending).toHaveLength(1);
    expect(pending[0].code).toBe(code);
    expect(pending[0].label).toBe('for alice');
  });

  it('validateCode succeeds with a valid code and approves the sender', () => {
    const code = manager.generateCode();
    const result = manager.validateCode('telegram', '12345', code);
    expect(result).toBe(true);
    expect(manager.isApproved('telegram', '12345')).toBe(true);
  });

  it('validateCode fails with an invalid code', () => {
    manager.generateCode();
    const result = manager.validateCode('telegram', '12345', 'ZZZZZZ');
    expect(result).toBe(false);
    expect(manager.isApproved('telegram', '12345')).toBe(false);
  });

  it('validateCode is case-insensitive', () => {
    const code = manager.generateCode();
    const result = manager.validateCode('telegram', '12345', code.toLowerCase());
    expect(result).toBe(true);
    expect(manager.isApproved('telegram', '12345')).toBe(true);
  });

  it('validateCode fails with an expired code', () => {
    vi.useFakeTimers();
    const code = manager.generateCode();

    // Advance time past the TTL
    vi.advanceTimersByTime(300_001);

    const result = manager.validateCode('telegram', '12345', code);
    expect(result).toBe(false);
    expect(manager.isApproved('telegram', '12345')).toBe(false);
  });

  it('validateCode removes the code after successful use', () => {
    const code = manager.generateCode();
    manager.validateCode('telegram', '12345', code);

    // Code should no longer be pending
    expect(manager.listPendingCodes()).toHaveLength(0);

    // Second use of the same code should fail
    const result = manager.validateCode('discord', '67890', code);
    expect(result).toBe(false);
  });

  it('isApproved returns true after validation', () => {
    const code = manager.generateCode();
    manager.validateCode('telegram', '12345', code);
    expect(manager.isApproved('telegram', '12345')).toBe(true);
  });

  it('isApproved returns false for unapproved senders', () => {
    expect(manager.isApproved('telegram', 'unknown')).toBe(false);
  });

  it('isApproved distinguishes between channels', () => {
    const code = manager.generateCode();
    manager.validateCode('telegram', '12345', code);
    expect(manager.isApproved('telegram', '12345')).toBe(true);
    expect(manager.isApproved('discord', '12345')).toBe(false);
  });

  it('revokeApproval removes approval', () => {
    const code = manager.generateCode();
    manager.validateCode('telegram', '12345', code);
    expect(manager.isApproved('telegram', '12345')).toBe(true);

    const revoked = manager.revokeApproval('telegram', '12345');
    expect(revoked).toBe(true);
    expect(manager.isApproved('telegram', '12345')).toBe(false);
  });

  it('revokeApproval returns false for non-existent sender', () => {
    const revoked = manager.revokeApproval('telegram', 'nonexistent');
    expect(revoked).toBe(false);
  });

  it('listApproved returns all approved senders', () => {
    const code1 = manager.generateCode();
    const code2 = manager.generateCode();
    manager.validateCode('telegram', '111', code1);
    manager.validateCode('discord', '222', code2);

    const approved = manager.listApproved();
    expect(approved).toHaveLength(2);
    expect(approved.map(a => a.senderId)).toContain('111');
    expect(approved.map(a => a.senderId)).toContain('222');
  });

  it('listPendingCodes returns only non-expired codes', () => {
    vi.useFakeTimers();
    const code1 = manager.generateCode('first');

    // Advance time so the first code is almost expired
    vi.advanceTimersByTime(200_000);
    const code2 = manager.generateCode('second');

    // Advance past first code's expiry
    vi.advanceTimersByTime(100_001);

    const pending = manager.listPendingCodes();
    expect(pending).toHaveLength(1);
    expect(pending[0].code).toBe(code2);
    expect(pending[0].label).toBe('second');
  });

  it('cleanup removes expired codes', () => {
    vi.useFakeTimers();
    manager.generateCode();

    vi.advanceTimersByTime(300_001);
    manager.cleanup();

    expect(manager.listPendingCodes()).toHaveLength(0);
  });

  it('enabled getter reflects config', () => {
    expect(manager.enabled).toBe(true);

    const disabled = new PairingManager({
      enabled: false,
      codeTtl: 300_000,
      codeLength: 6,
    });
    expect(disabled.enabled).toBe(false);
  });

  describe('PairingManager with store', () => {
    it('calls store.saveApproved when a code is validated', () => {
      const store: PairingStore = {
        loadApproved: () => [],
        saveApproved: vi.fn(),
        removeApproved: vi.fn(),
      };
      const mgr = new PairingManager({ enabled: true, codeTtl: 300_000, codeLength: 6 }, store);
      const code = mgr.generateCode();
      mgr.validateCode('telegram', '123', code);
      expect(store.saveApproved).toHaveBeenCalledWith(expect.objectContaining({
        channel: 'telegram',
        senderId: '123',
      }));
    });

    it('calls store.removeApproved when approval is revoked', () => {
      const store: PairingStore = {
        loadApproved: () => [],
        saveApproved: vi.fn(),
        removeApproved: vi.fn(),
      };
      const mgr = new PairingManager({ enabled: true, codeTtl: 300_000, codeLength: 6 }, store);
      const code = mgr.generateCode();
      mgr.validateCode('telegram', '123', code);
      mgr.revokeApproval('telegram', '123');
      expect(store.removeApproved).toHaveBeenCalledWith('telegram', '123');
    });

    it('loads approved senders from store on construction', () => {
      const store: PairingStore = {
        loadApproved: () => [
          { channel: 'telegram', senderId: '111', approvedAt: Date.now(), codeUsed: 'AAAAAA' },
          { channel: 'discord', senderId: '222', approvedAt: Date.now(), codeUsed: 'BBBBBB' },
        ],
        saveApproved: vi.fn(),
        removeApproved: vi.fn(),
      };
      const mgr = new PairingManager({ enabled: true, codeTtl: 300_000, codeLength: 6 }, store);
      expect(mgr.isApproved('telegram', '111')).toBe(true);
      expect(mgr.isApproved('discord', '222')).toBe(true);
      expect(mgr.listApproved()).toHaveLength(2);
    });

    it('works without a store (backward-compatible)', () => {
      const mgr = new PairingManager({ enabled: true, codeTtl: 300_000, codeLength: 6 });
      const code = mgr.generateCode();
      mgr.validateCode('telegram', '123', code);
      expect(mgr.isApproved('telegram', '123')).toBe(true);
    });
  });
});
