/**
 * Notification Manager – componentType-aware label tests
 *
 * Verifies that calendar notifications use "Task" for VTODO
 * and "Event" for VEVENT (or when componentType is absent).
 */

// ── Mocks ─────────────────────────────────────────────────────────────────

vi.mock('../../src/utils/platform.js', () => ({
  isTauri: false,
}));

vi.mock('../../src/utils/notification-bridge.js', () => ({
  notify: vi.fn(() => Promise.resolve()),
  requestPermission: vi.fn(() => Promise.resolve('granted')),
}));

vi.mock('../../src/utils/tauri-bridge.js', () => ({
  setBadgeCount: vi.fn(),
}));

vi.mock('../../src/utils/favicon-badge.js', () => ({
  updateFaviconBadge: vi.fn(),
}));

vi.mock('../../src/utils/remote.js', () => ({
  Remote: { request: vi.fn() },
}));

vi.mock('../../src/utils/sync-helpers.ts', () => ({
  extractFromField: vi.fn(() => ''),
}));

// handleNewMessage does a dynamic import of mailboxStore; mock it so the
// 2.4k-line module doesn't get loaded (and race vi.waitFor) in parallel runs.
vi.mock('../../src/stores/mailboxStore', () => ({
  mailboxStore: {
    state: {
      folders: { subscribe: (fn) => (fn([]), () => {}) },
    },
  },
}));

vi.mock('../../src/utils/websocket-client', () => ({
  WS_EVENTS: {
    NEW_MESSAGE: 'newMessage',
    MESSAGES_MOVED: 'messagesMoved',
    MESSAGES_COPIED: 'messagesCopied',
    FLAGS_UPDATED: 'flagsUpdated',
    MESSAGES_EXPUNGED: 'messagesExpunged',
    MAILBOX_CREATED: 'mailboxCreated',
    MAILBOX_DELETED: 'mailboxDeleted',
    MAILBOX_RENAMED: 'mailboxRenamed',
    CALENDAR_CREATED: 'calendarCreated',
    CALENDAR_UPDATED: 'calendarUpdated',
    CALENDAR_DELETED: 'calendarDeleted',
    CALENDAR_EVENT_CREATED: 'calendarEventCreated',
    CALENDAR_EVENT_UPDATED: 'calendarEventUpdated',
    CALENDAR_EVENT_DELETED: 'calendarEventDeleted',
    ADDRESS_BOOK_CREATED: 'addressBookCreated',
    ADDRESS_BOOK_DELETED: 'addressBookDeleted',
    CONTACT_CREATED: 'contactCreated',
    CONTACT_UPDATED: 'contactUpdated',
    CONTACT_DELETED: 'contactDeleted',
    NEW_RELEASE: 'newRelease',
  },
}));

import { notify } from '../../src/utils/notification-bridge.js';
import { Remote } from '../../src/utils/remote.js';
import { extractFromField } from '../../src/utils/sync-helpers.ts';
import {
  connectNotifications,
  requestNotificationPermission,
} from '../../src/utils/notification-manager.js';

// ── Helpers ───────────────────────────────────────────────────────────────

// Capture event handlers registered via connectNotifications
function createMockWsClient() {
  const handlers = new Map();
  return {
    on: vi.fn((event, handler) => {
      if (!handlers.has(event)) handlers.set(event, []);
      handlers.get(event).push(handler);
      return vi.fn(); // unsub
    }),
    handlers,
    emit(event, data) {
      const fns = handlers.get(event) || [];
      for (const fn of fns) fn(data);
    },
  };
}

// ── Tests ─────────────────────────────────────────────────────────────────

describe('notification-manager calendar labels', () => {
  let wsClient;

  beforeEach(async () => {
    vi.clearAllMocks();
    // Grant notification permission
    await requestNotificationPermission();
    wsClient = createMockWsClient();
    connectNotifications(wsClient);
  });

  it('shows "Calendar Task Created" for VTODO componentType', async () => {
    wsClient.emit('calendarEventCreated', {
      id: 'todo-001',
      summary: 'Buy groceries',
      componentType: 'VTODO',
    });

    // Wait for async showNotification
    await vi.waitFor(() => {
      expect(notify).toHaveBeenCalled();
    });

    const call = vi.mocked(notify).mock.calls[0][0];
    expect(call.title).toBe('Calendar Task Created');
    expect(call.body).toContain('Buy groceries');
  });

  it('shows "Calendar Event Created" for VEVENT componentType', async () => {
    wsClient.emit('calendarEventCreated', {
      id: 'evt-001',
      summary: 'Team Meeting',
      componentType: 'VEVENT',
    });

    await vi.waitFor(() => {
      expect(notify).toHaveBeenCalled();
    });

    const call = vi.mocked(notify).mock.calls[0][0];
    expect(call.title).toBe('Calendar Event Created');
    expect(call.body).toContain('Team Meeting');
  });

  it('defaults to "Calendar Event Created" when componentType is absent', async () => {
    wsClient.emit('calendarEventCreated', {
      id: 'evt-002',
      summary: 'Lunch',
    });

    await vi.waitFor(() => {
      expect(notify).toHaveBeenCalled();
    });

    const call = vi.mocked(notify).mock.calls[0][0];
    expect(call.title).toBe('Calendar Event Created');
  });

  it('shows "Calendar Task Updated" for VTODO componentType', async () => {
    wsClient.emit('calendarEventUpdated', {
      id: 'todo-002',
      summary: 'Buy groceries (updated)',
      componentType: 'VTODO',
    });

    await vi.waitFor(() => {
      expect(notify).toHaveBeenCalled();
    });

    const call = vi.mocked(notify).mock.calls[0][0];
    expect(call.title).toBe('Calendar Task Updated');
    expect(call.body).toContain('Buy groceries (updated)');
  });

  it('shows "Calendar Event Updated" for VEVENT componentType', async () => {
    wsClient.emit('calendarEventUpdated', {
      id: 'evt-003',
      summary: 'Team Meeting (rescheduled)',
      componentType: 'VEVENT',
    });

    await vi.waitFor(() => {
      expect(notify).toHaveBeenCalled();
    });

    const call = vi.mocked(notify).mock.calls[0][0];
    expect(call.title).toBe('Calendar Event Updated');
    expect(call.body).toContain('Team Meeting (rescheduled)');
  });

  it('defaults to "Calendar Event Updated" when componentType is absent', async () => {
    wsClient.emit('calendarEventUpdated', {
      id: 'evt-004',
      summary: 'Some event',
    });

    await vi.waitFor(() => {
      expect(notify).toHaveBeenCalled();
    });

    const call = vi.mocked(notify).mock.calls[0][0];
    expect(call.title).toBe('Calendar Event Updated');
  });

  it('uses "New task" as default body for VTODO without summary', async () => {
    wsClient.emit('calendarEventCreated', {
      id: 'todo-003',
      componentType: 'VTODO',
    });

    await vi.waitFor(() => {
      expect(notify).toHaveBeenCalled();
    });

    const call = vi.mocked(notify).mock.calls[0][0];
    expect(call.title).toBe('Calendar Task Created');
    expect(call.body).toContain('New task');
  });

  it('uses "New event" as default body for VEVENT without summary', async () => {
    wsClient.emit('calendarEventCreated', {
      id: 'evt-005',
      componentType: 'VEVENT',
    });

    await vi.waitFor(() => {
      expect(notify).toHaveBeenCalled();
    });

    const call = vi.mocked(notify).mock.calls[0][0];
    expect(call.title).toBe('Calendar Event Created');
    expect(call.body).toContain('New event');
  });

  it('uses "Task updated" as default body for VTODO update without summary', async () => {
    wsClient.emit('calendarEventUpdated', {
      id: 'todo-004',
      componentType: 'VTODO',
    });

    await vi.waitFor(() => {
      expect(notify).toHaveBeenCalled();
    });

    const call = vi.mocked(notify).mock.calls[0][0];
    expect(call.title).toBe('Calendar Task Updated');
    expect(call.body).toContain('Task updated');
  });

  it('extracts calendar summaries from nested ICS payloads and builds a task deep link', async () => {
    wsClient.emit('calendarEventCreated', {
      id: 'todo-ics',
      componentType: 'VTODO',
      event: {
        ical: 'BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nSUMMARY:Finish taxes\r\nEND:VTODO\r\nEND:VCALENDAR',
      },
    });

    await vi.waitFor(() => {
      expect(notify).toHaveBeenCalled();
    });

    const call = vi.mocked(notify).mock.calls[0][0];
    expect(call.body).toContain('Finish taxes');
    expect(call.data.path).toBe('#calendar');
    expect(call.data.url).toBe('forwardemail://calendar#task=todo-ics');
  });

  it('routes updated contacts to the contacts screen with an item-specific deep link', async () => {
    wsClient.emit('contactUpdated', {
      contact: {
        id: 'contact-123',
        fn: 'Alice Example',
      },
    });

    await vi.waitFor(() => {
      expect(notify).toHaveBeenCalled();
    });

    const call = vi.mocked(notify).mock.calls[0][0];
    expect(call.title).toBe('Contact Updated');
    expect(call.body).toContain('Alice Example');
    expect(call.data.path).toBe('#contacts');
    expect(call.data.url).toBe('forwardemail://contacts#contact=contact-123');
  });
});

describe('notification-manager new message routing payloads', () => {
  let wsClient;

  beforeEach(async () => {
    vi.clearAllMocks();
    // Simulate background so handleNewMessage fires OS notification (not toast)
    Object.defineProperty(document, 'visibilityState', {
      value: 'hidden',
      writable: true,
      configurable: true,
    });
    await requestNotificationPermission();
    wsClient = createMockWsClient();
    connectNotifications(wsClient);
  });

  afterEach(() => {
    Object.defineProperty(document, 'visibilityState', {
      value: 'visible',
      writable: true,
      configurable: true,
    });
  });

  it('includes both a mailbox hash path and a Forward Email deep-link URL', async () => {
    wsClient.emit('newMessage', {
      mailbox: 'INBOX',
      message: {
        id: 4242,
        uid: 4242,
        subject: 'Quarterly update',
        from: {
          text: 'Alice Example <alice@example.com>',
        },
      },
    });

    await vi.waitFor(() => {
      expect(notify).toHaveBeenCalled();
    });

    const call = vi.mocked(notify).mock.calls[0][0];
    expect(call.data.path).toBe('#inbox/4242');
    expect(call.data.url).toBe('forwardemail://mailbox#inbox/4242');
    expect(call.title).toContain('Alice Example');
  });
});

describe('notification-manager new message sender resolution', () => {
  let wsClient;

  beforeEach(async () => {
    vi.clearAllMocks();
    // Simulate background so handleNewMessage fires OS notification (not toast)
    Object.defineProperty(document, 'visibilityState', {
      value: 'hidden',
      writable: true,
      configurable: true,
    });
    await requestNotificationPermission();
    wsClient = createMockWsClient();
    connectNotifications(wsClient);
  });

  afterEach(() => {
    Object.defineProperty(document, 'visibilityState', {
      value: 'visible',
      writable: true,
      configurable: true,
    });
    vi.mocked(extractFromField).mockImplementation(() => '');
  });

  it('falls back to extractFromField for payload shapes the simple chain misses', async () => {
    // Real payloads can carry the mailparser shape {value: [{name, address}]},
    // which has none of .text/.name/.address at the top level. The old chain
    // returned null here and the notification said "Unknown sender".
    vi.mocked(extractFromField).mockImplementation((m) =>
      m?.from?.value?.[0] ? `${m.from.value[0].name} <${m.from.value[0].address}>` : '',
    );
    wsClient.emit('newMessage', {
      mailbox: 'INBOX',
      message: {
        id: 7001,
        uid: 7001,
        subject: 'Hello',
        from: { value: [{ name: 'Carol Vega', address: 'carol@example.com' }] },
      },
    });

    await vi.waitFor(() => expect(notify).toHaveBeenCalled());
    expect(vi.mocked(notify).mock.calls[0][0].title).toContain('Carol Vega');
  });

  it('does not attribute the previous newest message when the indexer lags', async () => {
    vi.mocked(extractFromField).mockImplementation((m) => m?.from?.text || '');
    // The WS broadcast beats the indexer: the API still returns only the
    // older message. The fallback must not blindly read list[0].
    vi.mocked(Remote.request).mockResolvedValue([
      { id: 6999, subject: 'Old news', from: { text: 'Wrong Sender <wrong@example.com>' } },
    ]);
    wsClient.emit('newMessage', {
      mailbox: 'INBOX',
      message: { id: 7002, uid: 7002, subject: 'Fresh' },
    });

    await vi.waitFor(() => expect(notify).toHaveBeenCalled());
    const call = vi.mocked(notify).mock.calls[0][0];
    expect(call.title).not.toContain('Wrong Sender');
    expect(call.title).toContain('Unknown sender');
  });
});
